From 955332bf02a7e5c7c17082eddeff9820783e99c2 Mon Sep 17 00:00:00 2001 From: Eugenio Pignataro Date: Mon, 6 Jul 2020 09:03:45 -0300 Subject: TokensFix: No camera render --- oscurart_tools/render/render_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oscurart_tools/render/render_tokens.py b/oscurart_tools/render/render_tokens.py index 3ae7bf5d..724cc843 100644 --- a/oscurart_tools/render/render_tokens.py +++ b/oscurart_tools/render/render_tokens.py @@ -30,7 +30,7 @@ def replaceTokens (dummy): "$Scene":bpy.context.scene.name, "$File":os.path.basename(bpy.data.filepath).split(".")[0], "$ViewLayer":bpy.context.view_layer.name, - "$Camera":bpy.context.scene.camera.name} + "$Camera": "NoCamera" if bpy.context.scene.camera == None else bpy.context.scene.camera.name} renpath = bpy.context.scene.render.filepath -- cgit v1.2.3 From 01186b0df9c54eefea95cf8861b8f5fb9960ddf3 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Fri, 10 Jul 2020 12:49:27 +0200 Subject: gltf exporter: Fix T76677 primitive extraction code refactoring --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_extract.py | 813 ++++++++------------- ..._blender_KHR_materials_pbrSpecularGlossiness.py | 3 +- 3 files changed, 295 insertions(+), 523 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 3ea1ce11..2e19cbeb 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 28), + "version": (1, 3, 29), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index e2d224ce..5fe719ee 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -18,40 +18,22 @@ from mathutils import Vector, Quaternion, Matrix from mathutils.geometry import tessellate_polygon -from operator import attrgetter from . import gltf2_blender_export_keys from ...io.com.gltf2_io_debug import print_console from ...io.com.gltf2_io_color_management import color_srgb_to_scene_linear from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins -# -# Globals -# - -INDICES_ID = 'indices' -MATERIAL_ID = 'material' -ATTRIBUTES_ID = 'attributes' - -COLOR_PREFIX = 'COLOR_' -MORPH_TANGENT_PREFIX = 'MORPH_TANGENT_' -MORPH_NORMAL_PREFIX = 'MORPH_NORMAL_' -MORPH_POSITION_PREFIX = 'MORPH_POSITION_' -TEXCOORD_PREFIX = 'TEXCOORD_' -WEIGHTS_PREFIX = 'WEIGHTS_' -JOINTS_PREFIX = 'JOINTS_' - -TANGENT_ATTRIBUTE = 'TANGENT' -NORMAL_ATTRIBUTE = 'NORMAL' -POSITION_ATTRIBUTE = 'POSITION' - -GLTF_MAX_COLORS = 2 - # # Classes # +class Prim: + def __init__(self): + self.verts = {} + self.indices = [] + class ShapeKey: def __init__(self, shape_key, vertex_normals, polygon_normals): self.shape_key = shape_key @@ -110,17 +92,17 @@ def convert_swizzle_tangent(tan, armature, blender_object, export_settings): if (not armature) or (not blender_object): # Classic case. Mesh is not skined, no need to apply armature transfoms on vertices / normals / tangents if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((tan[0], tan[2], -tan[1], 1.0)) + return Vector((tan[0], tan[2], -tan[1])) else: - return Vector((tan[0], tan[1], tan[2], 1.0)) + return Vector((tan[0], tan[1], tan[2])) else: # Mesh is skined, we have to apply armature transforms on data apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world - new_tan = apply_matrix.to_quaternion() @ tan + new_tan = apply_matrix.to_quaternion() @ Vector((tan[0], tan[1], tan[2])) if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((new_tan[0], new_tan[2], -new_tan[1], 1.0)) + return Vector((new_tan[0], new_tan[2], -new_tan[1])) else: - return Vector((new_tan[0], new_tan[1], new_tan[2], 1.0)) + return Vector((new_tan[0], new_tan[1], new_tan[2])) def convert_swizzle_rotation(rot, export_settings): """ @@ -151,173 +133,129 @@ def decompose_transition(matrix, export_settings): def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vertex_groups, modifiers, export_settings): """ Extract primitives from a mesh. Polygons are triangulated and sorted by material. - - Furthermore, primitives are split up, if the indices range is exceeded. - Finally, triangles are also split up/duplicated, if face normals are used instead of vertex normals. + Vertices in multiple faces get split up as necessary. """ print_console('INFO', 'Extracting primitive: ' + blender_mesh.name) - if blender_mesh.has_custom_normals: - # Custom normals are all (0, 0, 0) until calling calc_normals_split() or calc_tangents(). - blender_mesh.calc_normals_split() - - use_tangents = False - if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0: - try: - blender_mesh.calc_tangents() - use_tangents = True - except Exception: - print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.') - # - - material_map = {} - + # First, decide what attributes to gather (eg. how many COLOR_n, etc.) + # Also calculate normals/tangents now if necessary. # - # Gathering position, normal and tex_coords. - # - no_material_attributes = { - POSITION_ATTRIBUTE: [], - NORMAL_ATTRIBUTE: [] - } - - if use_tangents: - no_material_attributes[TANGENT_ATTRIBUTE] = [] - # - # Directory of materials with its primitive. - # - no_material_primitives = { - MATERIAL_ID: 0, - INDICES_ID: [], - ATTRIBUTES_ID: no_material_attributes - } + use_normals = export_settings[gltf2_blender_export_keys.NORMALS] + if use_normals: + if blender_mesh.has_custom_normals: + # Custom normals are all (0, 0, 0) until calling calc_normals_split() or calc_tangents(). + blender_mesh.calc_normals_split() - material_idx_to_primitives = {0: no_material_primitives} - - # - - vertex_index_to_new_indices = {} - - material_map[0] = vertex_index_to_new_indices - - # - # Create primitive for each material. - # - for (mat_idx, _) in enumerate(blender_mesh.materials): - attributes = { - POSITION_ATTRIBUTE: [], - NORMAL_ATTRIBUTE: [] - } - - if use_tangents: - attributes[TANGENT_ATTRIBUTE] = [] - - primitive = { - MATERIAL_ID: mat_idx, - INDICES_ID: [], - ATTRIBUTES_ID: attributes - } - - material_idx_to_primitives[mat_idx] = primitive - - # - - vertex_index_to_new_indices = {} - - material_map[mat_idx] = vertex_index_to_new_indices + use_tangents = False + if use_normals and export_settings[gltf2_blender_export_keys.TANGENTS]: + if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0: + try: + blender_mesh.calc_tangents() + use_tangents = True + except Exception: + print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.') tex_coord_max = 0 - if blender_mesh.uv_layers.active: - tex_coord_max = len(blender_mesh.uv_layers) - - # - - vertex_colors = {} + if export_settings[gltf2_blender_export_keys.TEX_COORDS]: + if blender_mesh.uv_layers.active: + tex_coord_max = len(blender_mesh.uv_layers) - color_index = 0 - for vertex_color in blender_mesh.vertex_colors: - vertex_color_name = COLOR_PREFIX + str(color_index) - vertex_colors[vertex_color_name] = vertex_color + color_max = 0 + if export_settings[gltf2_blender_export_keys.COLORS]: + color_max = len(blender_mesh.vertex_colors) - color_index += 1 - if color_index >= GLTF_MAX_COLORS: - break - color_max = color_index - - # - - bone_max = 0 - for blender_polygon in blender_mesh.polygons: - for loop_index in blender_polygon.loop_indices: - vertex_index = blender_mesh.loops[loop_index].vertex_index - bones_count = len(blender_mesh.vertices[vertex_index].groups) - if bones_count > 0: - if bones_count % 4 == 0: - bones_count -= 1 - bone_max = max(bone_max, bones_count // 4 + 1) - - # - - morph_max = 0 - - blender_shape_keys = [] - - if blender_mesh.shape_keys is not None: + bone_max = 0 # number of JOINTS_n sets needed (1 set = 4 influences) + armature = None + if blender_vertex_groups and export_settings[gltf2_blender_export_keys.SKINS]: + if modifiers is not None: + modifiers_dict = {m.type: m for m in modifiers} + if "ARMATURE" in modifiers_dict: + modifier = modifiers_dict["ARMATURE"] + armature = modifier.object + + # Skin must be ignored if the object is parented to a bone of the armature + # (This creates an infinite recursive error) + # So ignoring skin in that case + is_child_of_arma = ( + armature and + blender_object and + blender_object.parent_type == "BONE" and + blender_object.parent.name == armature.name + ) + if is_child_of_arma: + armature = None + + if armature: + skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings) + if not skin: + armature = None + else: + joint_name_to_index = {joint.name: index for index, joint in enumerate(skin.joints)} + group_to_joint = [joint_name_to_index.get(g.name) for g in blender_vertex_groups] + + # Find out max number of bone influences + for blender_polygon in blender_mesh.polygons: + for loop_index in blender_polygon.loop_indices: + vertex_index = blender_mesh.loops[loop_index].vertex_index + groups_count = len(blender_mesh.vertices[vertex_index].groups) + bones_count = (groups_count + 3) // 4 + bone_max = max(bone_max, bones_count) + + use_morph_normals = use_normals and export_settings[gltf2_blender_export_keys.MORPH_NORMAL] + use_morph_tangents = use_morph_normals and use_tangents and export_settings[gltf2_blender_export_keys.MORPH_TANGENT] + + shape_keys = [] + if blender_mesh.shape_keys and export_settings[gltf2_blender_export_keys.MORPH]: for blender_shape_key in blender_mesh.shape_keys.key_blocks: - if blender_shape_key != blender_shape_key.relative_key: - if blender_shape_key.mute is False: - morph_max += 1 - blender_shape_keys.append(ShapeKey( - blender_shape_key, - blender_shape_key.normals_vertex_get(), # calculate vertex normals for this shape key - blender_shape_key.normals_polygon_get())) # calculate polygon normals for this shape key - + if blender_shape_key == blender_shape_key.relative_key or blender_shape_key.mute: + continue + if use_morph_normals: + vertex_normals = blender_shape_key.normals_vertex_get() + polygon_normals = blender_shape_key.normals_polygon_get() + else: + vertex_normals = None + polygon_normals = None + shape_keys.append(ShapeKey( + blender_shape_key, + vertex_normals, + polygon_normals, + )) - armature = None - if modifiers is not None: - modifiers_dict = {m.type: m for m in modifiers} - if "ARMATURE" in modifiers_dict: - modifier = modifiers_dict["ARMATURE"] - armature = modifier.object + use_materials = export_settings[gltf2_blender_export_keys.MATERIALS] # - # Convert polygon to primitive indices and eliminate invalid ones. Assign to material. + # Gather the verts and indices for each primitive. # - for blender_polygon in blender_mesh.polygons: - export_color = True - # + prims = {} - if export_settings['gltf_materials'] is False: - primitive = material_idx_to_primitives[0] - vertex_index_to_new_indices = material_map[0] - elif not blender_polygon.material_index in material_idx_to_primitives: - primitive = material_idx_to_primitives[0] - vertex_index_to_new_indices = material_map[0] - else: - primitive = material_idx_to_primitives[blender_polygon.material_index] - vertex_index_to_new_indices = material_map[blender_polygon.material_index] - # - - attributes = primitive[ATTRIBUTES_ID] - - face_normal = blender_polygon.normal - face_tangent = Vector((0.0, 0.0, 0.0)) - face_bitangent = Vector((0.0, 0.0, 0.0)) - if use_tangents: - for loop_index in blender_polygon.loop_indices: - temp_vertex = blender_mesh.loops[loop_index] - face_tangent += temp_vertex.tangent - face_bitangent += temp_vertex.bitangent - - face_tangent.normalize() - face_bitangent.normalize() - - # - - indices = primitive[INDICES_ID] + for blender_polygon in blender_mesh.polygons: + material_idx = -1 + if use_materials: + material_idx = blender_polygon.material_index + + prim = prims.get(material_idx) + if not prim: + prim = Prim() + prims[material_idx] = prim + + if use_normals: + face_normal = None + if not (blender_polygon.use_smooth or blender_mesh.use_auto_smooth): + # Calc face normal/tangents + face_normal = blender_polygon.normal + if use_tangents: + face_tangent = Vector((0.0, 0.0, 0.0)) + face_bitangent = Vector((0.0, 0.0, 0.0)) + for loop_index in blender_polygon.loop_indices: + loop = blender_mesh.loops[loop_index] + face_tangent += loop.tangent + face_bitangent += loop.bitangent + face_tangent.normalize() + face_bitangent.normalize() loop_index_list = [] @@ -335,7 +273,6 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert triangles = tessellate_polygon((polyline,)) for triangle in triangles: - for triangle_index in triangle: loop_index_list.append(blender_polygon.loop_indices[triangle_index]) else: @@ -343,366 +280,200 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert for loop_index in loop_index_list: vertex_index = blender_mesh.loops[loop_index].vertex_index + vertex = blender_mesh.vertices[vertex_index] - if vertex_index_to_new_indices.get(vertex_index) is None: - vertex_index_to_new_indices[vertex_index] = [] - - # - - v = None - n = None - t = None - b = None - uvs = [] - colors = [] - joints = [] - weights = [] - - target_positions = [] - target_normals = [] - target_tangents = [] + # vert will be a tuple of all the vertex attributes. + # Used as cache key in prim.verts. + vert = (vertex_index,) - vertex = blender_mesh.vertices[vertex_index] + v = vertex.co + vert += ((v[0], v[1], v[2]),) - v = convert_swizzle_location(vertex.co, armature, blender_object, export_settings) - if blender_polygon.use_smooth or blender_mesh.use_auto_smooth: - if blender_mesh.has_custom_normals: - n = convert_swizzle_normal(blender_mesh.loops[loop_index].normal, armature, blender_object, export_settings) + if use_normals: + if face_normal is None: + if blender_mesh.has_custom_normals: + n = blender_mesh.loops[loop_index].normal + else: + n = vertex.normal + if use_tangents: + t = blender_mesh.loops[loop_index].tangent + b = blender_mesh.loops[loop_index].bitangent else: - n = convert_swizzle_normal(vertex.normal, armature, blender_object, export_settings) - if use_tangents: - t = convert_swizzle_tangent(blender_mesh.loops[loop_index].tangent, armature, blender_object, export_settings) - b = convert_swizzle_location(blender_mesh.loops[loop_index].bitangent, armature, blender_object, export_settings) - else: - n = convert_swizzle_normal(face_normal, armature, blender_object, export_settings) + n = face_normal + if use_tangents: + t = face_tangent + b = face_bitangent + vert += ((n[0], n[1], n[2]),) if use_tangents: - t = convert_swizzle_tangent(face_tangent, armature, blender_object, export_settings) - b = convert_swizzle_location(face_bitangent, armature, blender_object, export_settings) - - if use_tangents: - tv = Vector((t[0], t[1], t[2])) - bv = Vector((b[0], b[1], b[2])) - nv = Vector((n[0], n[1], n[2])) - - if (nv.cross(tv)).dot(bv) < 0.0: - t[3] = -1.0 - - if blender_mesh.uv_layers.active: - for tex_coord_index in range(0, tex_coord_max): - uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv - uvs.append([uv.x, 1.0 - uv.y]) - - # - - if color_max > 0 and export_color: - for color_index in range(0, color_max): - color_name = COLOR_PREFIX + str(color_index) - color = vertex_colors[color_name].data[loop_index].color - colors.append([ - color_srgb_to_scene_linear(color[0]), - color_srgb_to_scene_linear(color[1]), - color_srgb_to_scene_linear(color[2]), - color[3] - ]) - - # - - bone_count = 0 - - # Skin must be ignored if the object is parented to a bone of the armature - # (This creates an infinite recursive error) - # So ignoring skin in that case - if blender_object and blender_object.parent_type == "BONE" and blender_object.parent.name == armature.name: - bone_max = 0 # joints & weights will be ignored in following code - else: - # Manage joints & weights - if blender_vertex_groups is not None and vertex.groups is not None and len(vertex.groups) > 0 and export_settings[gltf2_blender_export_keys.SKINS]: - joint = [] - weight = [] - vertex_groups = vertex.groups - if not export_settings['gltf_all_vertex_influences']: - # sort groups by weight descending - vertex_groups = sorted(vertex.groups, key=attrgetter('weight'), reverse=True) - for group_element in vertex_groups: - - if len(joint) == 4: - bone_count += 1 - joints.append(joint) - weights.append(weight) - joint = [] - weight = [] - - # - - joint_weight = group_element.weight - if joint_weight <= 0.0: + vert += ((t[0], t[1], t[2]),) + vert += ((b[0], b[1], b[2]),) + # TODO: store just bitangent_sign in vert, not whole bitangent? + + for tex_coord_index in range(0, tex_coord_max): + uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv + uv = (uv.x, 1.0 - uv.y) + vert += (uv,) + + for color_index in range(0, color_max): + color = blender_mesh.vertex_colors[color_index].data[loop_index].color + col = ( + color_srgb_to_scene_linear(color[0]), + color_srgb_to_scene_linear(color[1]), + color_srgb_to_scene_linear(color[2]), + color[3], + ) + vert += (col,) + + if bone_max: + bones = [] + if vertex.groups: + for group_element in vertex.groups: + weight = group_element.weight + if weight <= 0.0: continue - - # - - vertex_group_index = group_element.group - - if vertex_group_index < 0 or vertex_group_index >= len(blender_vertex_groups): + try: + joint = group_to_joint[group_element.group] + except Exception: continue - vertex_group_name = blender_vertex_groups[vertex_group_index].name - - joint_index = None - - if armature: - skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings) - for index, j in enumerate(skin.joints): - if j.name == vertex_group_name: - joint_index = index - break - - # - if joint_index is not None: - joint.append(joint_index) - weight.append(joint_weight) - - if len(joint) > 0: - bone_count += 1 - - for fill in range(0, 4 - len(joint)): - joint.append(0) - weight.append(0.0) - - joints.append(joint) - weights.append(weight) - - for fill in range(0, bone_max - bone_count): - joints.append([0, 0, 0, 0]) - weights.append([0.0, 0.0, 0.0, 0.0]) - - # - - if morph_max > 0 and export_settings[gltf2_blender_export_keys.MORPH]: - for morph_index in range(0, morph_max): - blender_shape_key = blender_shape_keys[morph_index] - - v_morph = convert_swizzle_location(blender_shape_key.shape_key.data[vertex_index].co, - armature, blender_object, - export_settings) - - # Store delta. - v_morph -= v - - target_positions.append(v_morph) - - # + if joint is None: + continue + bones.append((joint, weight)) + bones.sort(key=lambda x: x[1], reverse=True) + bones = tuple(bones) + vert += (bones,) - n_morph = None + for shape_key in shape_keys: + v_morph = shape_key.shape_key.data[vertex_index].co + v_morph = v_morph - v # store delta + vert += ((v_morph[0], v_morph[1], v_morph[2]),) + if use_morph_normals: if blender_polygon.use_smooth: - temp_normals = blender_shape_key.vertex_normals - n_morph = (temp_normals[vertex_index * 3 + 0], temp_normals[vertex_index * 3 + 1], - temp_normals[vertex_index * 3 + 2]) + normals = shape_key.vertex_normals + n_morph = Vector(( + normals[vertex_index * 3 + 0], + normals[vertex_index * 3 + 1], + normals[vertex_index * 3 + 2], + )) else: - temp_normals = blender_shape_key.polygon_normals - n_morph = ( - temp_normals[blender_polygon.index * 3 + 0], temp_normals[blender_polygon.index * 3 + 1], - temp_normals[blender_polygon.index * 3 + 2]) - - n_morph = convert_swizzle_normal(Vector(n_morph), armature, blender_object, export_settings) - - # Store delta. - n_morph -= n - - target_normals.append(n_morph) - - # - - if use_tangents: - rotation = n_morph.rotation_difference(n) - - t_morph = Vector((t[0], t[1], t[2])) + normals = shape_key.polygon_normals + n_morph = Vector(( + normals[blender_polygon.index * 3 + 0], + normals[blender_polygon.index * 3 + 1], + normals[blender_polygon.index * 3 + 2], + )) + n_morph = n_morph - n # store delta + vert += ((n_morph[0], n_morph[1], n_morph[2]),) + + vert_idx = prim.verts.setdefault(vert, len(prim.verts)) + prim.indices.append(vert_idx) - t_morph.rotate(rotation) - - target_tangents.append(t_morph) - - # - # - - create = True + # + # Put the verts into attribute arrays. + # - for current_new_index in vertex_index_to_new_indices[vertex_index]: - found = True + result_primitives = [] - for i in range(0, 3): - if attributes[POSITION_ATTRIBUTE][current_new_index * 3 + i] != v[i]: - found = False - break + for material_idx, prim in prims.items(): + if not prim.indices: + continue - if attributes[NORMAL_ATTRIBUTE][current_new_index * 3 + i] != n[i]: - found = False - break + vs = [] + ns = [] + ts = [] + uvs = [[] for _ in range(tex_coord_max)] + cols = [[] for _ in range(color_max)] + joints = [[] for _ in range(bone_max)] + weights = [[] for _ in range(bone_max)] + vs_morph = [[] for _ in shape_keys] + ns_morph = [[] for _ in shape_keys] + ts_morph = [[] for _ in shape_keys] + + for vert in prim.verts.keys(): + i = 0 + + i += 1 # skip over Blender mesh index + + v = vert[i] + i += 1 + v = convert_swizzle_location(v, armature, blender_object, export_settings) + vs.extend(v) + + if use_normals: + n = vert[i] + i += 1 + n = convert_swizzle_normal(n, armature, blender_object, export_settings) + ns.extend(n) if use_tangents: - for i in range(0, 4): - if attributes[TANGENT_ATTRIBUTE][current_new_index * 4 + i] != t[i]: - found = False - break - - if not found: - continue - - for tex_coord_index in range(0, tex_coord_max): - uv = uvs[tex_coord_index] - - tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) - for i in range(0, 2): - if attributes[tex_coord_id][current_new_index * 2 + i] != uv[i]: - found = False - break - - if export_color: - for color_index in range(0, color_max): - color = colors[color_index] - - color_id = COLOR_PREFIX + str(color_index) - for i in range(0, 3): - # Alpha is always 1.0 - see above. - current_color = attributes[color_id][current_new_index * 4 + i] - if color_srgb_to_scene_linear(current_color) != color[i]: - found = False - break - - if export_settings[gltf2_blender_export_keys.SKINS]: - for bone_index in range(0, bone_max): - joint = joints[bone_index] - weight = weights[bone_index] - - joint_id = JOINTS_PREFIX + str(bone_index) - weight_id = WEIGHTS_PREFIX + str(bone_index) - for i in range(0, 4): - if attributes[joint_id][current_new_index * 4 + i] != joint[i]: - found = False - break - if attributes[weight_id][current_new_index * 4 + i] != weight[i]: - found = False - break - - if export_settings[gltf2_blender_export_keys.MORPH]: - for morph_index in range(0, morph_max): - target_position = target_positions[morph_index] - target_normal = target_normals[morph_index] - if use_tangents: - target_tangent = target_tangents[morph_index] - - target_position_id = MORPH_POSITION_PREFIX + str(morph_index) - target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) - target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) - for i in range(0, 3): - if attributes[target_position_id][current_new_index * 3 + i] != target_position[i]: - found = False - break - if attributes[target_normal_id][current_new_index * 3 + i] != target_normal[i]: - found = False - break - if use_tangents: - if attributes[target_tangent_id][current_new_index * 3 + i] != target_tangent[i]: - found = False - break - - if found: - indices.append(current_new_index) - - create = False - break - - if not create: - continue - - new_index = 0 - - if primitive.get('max_index') is not None: - new_index = primitive['max_index'] + 1 - - primitive['max_index'] = new_index - - vertex_index_to_new_indices[vertex_index].append(new_index) - - # - # - - indices.append(new_index) - - # - - attributes[POSITION_ATTRIBUTE].extend(v) - attributes[NORMAL_ATTRIBUTE].extend(n) - if use_tangents: - attributes[TANGENT_ATTRIBUTE].extend(t) - - if blender_mesh.uv_layers.active: - for tex_coord_index in range(0, tex_coord_max): - tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index) - - if attributes.get(tex_coord_id) is None: - attributes[tex_coord_id] = [] - - attributes[tex_coord_id].extend(uvs[tex_coord_index]) - - if export_color: - for color_index in range(0, color_max): - color_id = COLOR_PREFIX + str(color_index) - - if attributes.get(color_id) is None: - attributes[color_id] = [] - - attributes[color_id].extend(colors[color_index]) - - if export_settings[gltf2_blender_export_keys.SKINS]: - for bone_index in range(0, bone_max): - joint_id = JOINTS_PREFIX + str(bone_index) - - if attributes.get(joint_id) is None: - attributes[joint_id] = [] - - attributes[joint_id].extend(joints[bone_index]) - - weight_id = WEIGHTS_PREFIX + str(bone_index) - - if attributes.get(weight_id) is None: - attributes[weight_id] = [] - - attributes[weight_id].extend(weights[bone_index]) - - if export_settings[gltf2_blender_export_keys.MORPH]: - for morph_index in range(0, morph_max): - target_position_id = MORPH_POSITION_PREFIX + str(morph_index) - - if attributes.get(target_position_id) is None: - attributes[target_position_id] = [] - - attributes[target_position_id].extend(target_positions[morph_index]) - - target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index) - - if attributes.get(target_normal_id) is None: - attributes[target_normal_id] = [] - - attributes[target_normal_id].extend(target_normals[morph_index]) - - if use_tangents: - target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index) - - if attributes.get(target_tangent_id) is None: - attributes[target_tangent_id] = [] - - attributes[target_tangent_id].extend(target_tangents[morph_index]) - - # - # Add non-empty primitives - # - - result_primitives = [ - primitive - for primitive in material_idx_to_primitives.values() - if len(primitive[INDICES_ID]) != 0 - ] - - print_console('INFO', 'Primitives created: ' + str(len(result_primitives))) + t = vert[i] + i += 1 + t = convert_swizzle_tangent(t, armature, blender_object, export_settings) + ts.extend(t) + + b = vert[i] + i += 1 + b = convert_swizzle_tangent(b, armature, blender_object, export_settings) + b_sign = -1.0 if (Vector(n).cross(Vector(t))).dot(Vector(b)) < 0.0 else 1.0 + ts.append(b_sign) + + for tex_coord_index in range(0, tex_coord_max): + uv = vert[i] + i += 1 + uvs[tex_coord_index].extend(uv) + + for color_index in range(0, color_max): + col = vert[i] + i += 1 + cols[color_index].extend(col) + + if bone_max: + bones = vert[i] + i += 1 + for j in range(0, 4 * bone_max): + if j < len(bones): + joint, weight = bones[j] + else: + joint, weight = 0, 0.0 + joints[j//4].append(joint) + weights[j//4].append(weight) + + for shape_key_index in range(0, len(shape_keys)): + v_morph = vert[i] + i += 1 + v_morph = convert_swizzle_location(v_morph, armature, blender_object, export_settings) + vs_morph[shape_key_index].extend(v_morph) + + if use_morph_normals: + n_morph = vert[i] + i += 1 + n_morph = convert_swizzle_normal(n_morph, armature, blender_object, export_settings) + ns_morph[shape_key_index].extend(n_morph) + + if use_morph_tangents: + rotation = n_morph.rotation_difference(n) + t_morph = Vector(t) + t_morph.rotate(rotation) + ts_morph[shape_key_index].extend(t_morph) + + attributes = {} + attributes['POSITION'] = vs + if ns: attributes['NORMAL'] = ns + if ts: attributes['TANGENT'] = ts + for i, uv in enumerate(uvs): attributes['TEXCOORD_%d' % i] = uv + for i, col in enumerate(cols): attributes['COLOR_%d' % i] = col + for i, js in enumerate(joints): attributes['JOINTS_%d' % i] = js + for i, ws in enumerate(weights): attributes['WEIGHTS_%d' % i] = ws + for i, vm in enumerate(vs_morph): attributes['MORPH_POSITION_%d' % i] = vm + for i, nm in enumerate(ns_morph): attributes['MORPH_NORMAL_%d' % i] = nm + for i, tm in enumerate(ts_morph): attributes['MORPH_TANGENT_%d' % i] = tm + + result_primitives.append({ + 'attributes': attributes, + 'indices': prim.indices, + 'material': material_idx, + }) + + print_console('INFO', 'Primitives created: %d' % len(result_primitives)) return result_primitives diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py index ce5e1aed..bb1bf272 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py @@ -79,7 +79,8 @@ def pbr_specular_glossiness(mh): ) if mh.pymat.occlusion_texture is not None: - node = make_settings_node(mh, location=(610, -1060)) + node = make_settings_node(mh) + node.location = (610, -1060) occlusion( mh, location=(510, -970), -- cgit v1.2.3 From 48c8d6c23010fc73d62f44e366901f08680d08da Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Fri, 10 Jul 2020 12:55:01 +0200 Subject: glTF export: Fix T78754: export alpha scalar value (not coming from texture) --- io_scene_gltf2/__init__.py | 2 +- .../exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py | 7 +++++-- io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py | 9 +++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 2e19cbeb..211839f8 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 29), + "version": (1, 3, 30), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py index 54493799..7913d175 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py @@ -47,6 +47,9 @@ def __filter_pbr_material(blender_material, export_settings): def __gather_base_color_factor(blender_material, export_settings): + alpha_socket = gltf2_blender_get.get_socket(blender_material, "Alpha") + alpha = alpha_socket.default_value if alpha_socket is not None and not alpha_socket.is_linked else 1.0 + base_color_socket = gltf2_blender_get.get_socket(blender_material, "Base Color") if base_color_socket is None: base_color_socket = gltf2_blender_get.get_socket(blender_material, "BaseColor") @@ -57,7 +60,7 @@ def __gather_base_color_factor(blender_material, export_settings): if not isinstance(base_color_socket, bpy.types.NodeSocket): return None if not base_color_socket.is_linked: - return list(base_color_socket.default_value) + return list(base_color_socket.default_value)[:3] + [alpha] texture_node = __get_tex_from_socket(base_color_socket) if texture_node is None: @@ -85,7 +88,7 @@ def __gather_base_color_factor(blender_material, export_settings): .format(multiply_node.name)) return None - return list(factor_socket.default_value) + return list(factor_socket.default_value)[:3] + [alpha] def __gather_base_color_texture(blender_material, export_settings): diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py index 00bd08d2..deb9e301 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py @@ -233,7 +233,7 @@ def base_color( base_color_factor = [1, 1, 1, 1] if base_color_texture is None and not mh.vertex_color: - color_socket.default_value = base_color_factor + color_socket.default_value = base_color_factor[:3] + [1] if alpha_socket is not None: alpha_socket.default_value = base_color_factor[3] return @@ -242,10 +242,7 @@ def base_color( needs_color_factor = base_color_factor[:3] != [1, 1, 1] needs_alpha_factor = base_color_factor[3] != 1.0 and alpha_socket is not None if needs_color_factor or needs_alpha_factor: - # For now, always create the color factor node because the exporter - # reads the alpha value from here. Can get rid of "or needs_alpha_factor" - # when it learns to understand the alpha socket. - if needs_color_factor or needs_alpha_factor: + if needs_color_factor: node = mh.node_tree.nodes.new('ShaderNodeMixRGB') node.label = 'Color Factor' node.location = x - 140, y @@ -255,7 +252,7 @@ def base_color( # Inputs node.inputs['Fac'].default_value = 1.0 color_socket = node.inputs['Color1'] - node.inputs['Color2'].default_value = base_color_factor + node.inputs['Color2'].default_value = base_color_factor[:3] + [1] if needs_alpha_factor: node = mh.node_tree.nodes.new('ShaderNodeMath') -- cgit v1.2.3 From 943d41d4438623e992b3c6919a2bb5a2b6818ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 1 Jul 2020 18:24:37 +0200 Subject: BlenderKit: fix search by author verification status fix ratings drawing for not logged in users fix ratings update function(was reacting to quality rating) --- blenderkit/__init__.py | 3 ++- blenderkit/ratings.py | 2 +- blenderkit/ui.py | 2 ++ blenderkit/ui_panels.py | 10 ++++++---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index 449e65a4..d511bbf8 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -481,6 +481,7 @@ class BlenderKitCommonSearchProps(object): ('DELETED', 'Deleted', 'Deleted'), ), default='ALL', + update=search.search_update, ) @@ -655,7 +656,7 @@ class BlenderKitRatingProps(PropertyGroup): rating_work_hours: FloatProperty(name="Work Hours", description="How many hours did this work take?", - default=0.01, + default=0.00, min=0.0, max=1000, update=ratings.update_ratings_work_hours ) rating_complexity: IntProperty(name="Complexity", diff --git a/blenderkit/ratings.py b/blenderkit/ratings.py index 38dbbaf8..dbe8b8eb 100644 --- a/blenderkit/ratings.py +++ b/blenderkit/ratings.py @@ -128,7 +128,7 @@ def update_ratings_work_hours(self, context): bkit_ratings = asset.bkit_ratings url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' - if bkit_ratings.rating_quality > 0.1: + if bkit_ratings.rating_work_hours > 0.05: ratings = [('working_hours', round(bkit_ratings.rating_work_hours, 1))] tasks_queue.add_task((send_rating_to_thread_work_hours, (url, ratings, headers)), wait=1, only_last=True) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index f56dad77..0a6b96a8 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1688,10 +1688,12 @@ class AssetBarOperator(bpy.types.Operator): if a is not None: sprops = utils.get_search_props() sprops.search_keywords = '' + sprops.search_verification_status = 'ALL' utils.p('author:', a) search.search(author_id=a) return {'RUNNING_MODAL'} if event.type == 'X' and ui_props.active_index > -1: + # delete downloaded files for this asset sr = bpy.context.scene['search results'] asset_data = sr[ui_props.active_index] print(asset_data['name']) diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index 4efe732f..7de2a240 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -74,15 +74,16 @@ def draw_ratings(layout, context): if asset == None: return; + col = layout.column() if not utils.user_logged_in(): label_multiline(layout, text='Please login or sign up ' 'to rate assets.') - return + col.enabled = False bkit_ratings = asset.bkit_ratings - ratings.draw_rating(layout, bkit_ratings, 'rating_quality', 'Quality') - layout.separator() - layout.prop(bkit_ratings, 'rating_work_hours') + ratings.draw_rating(col, bkit_ratings, 'rating_quality', 'Quality') + col.separator() + col.prop(bkit_ratings, 'rating_work_hours') w = context.region.width # layout.label(text='problems') @@ -93,6 +94,7 @@ def draw_ratings(layout, context): # row = layout.row() # op = row.operator("object.blenderkit_rating_upload", text="Send rating", icon='URL') # return op + #re-enable layout if included in longer panel def draw_not_logged_in(source): -- cgit v1.2.3 From 37c2aacdb240fc6fd279d9f3b53cd37b88d90bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 1 Jul 2020 21:26:03 +0200 Subject: BlenderKit: switch thumbnails to sRGB previously, Blender was double color correcting the images, so these were set to linear by the addon. This was obviously fixed, so BlenderKit thumbnails looked lighter. --- blenderkit/search.py | 2 +- blenderkit/ui.py | 2 +- blenderkit/utils.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/blenderkit/search.py b/blenderkit/search.py index 474630e6..d77784d9 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -411,7 +411,7 @@ def load_previews(): img.unpack(method='USE_ORIGINAL') img.filepath = tpath img.reload() - img.colorspace_settings.name = 'Linear' + img.colorspace_settings.name = 'sRGB' i += 1 # print('previews loaded') diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 0a6b96a8..f13b4778 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -876,7 +876,7 @@ def draw_callback_2d_search(self, context): else: iname = utils.previmg_name(ui_props.active_index) img = bpy.data.images.get(iname) - img.colorspace_settings.name = 'Linear' + img.colorspace_settings.name = 'sRGB' gimg = None atip = '' diff --git a/blenderkit/utils.py b/blenderkit/utils.py index 289ec817..5544ba3e 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -289,12 +289,12 @@ def get_hidden_image(tpath, bdata_name, force_reload=False): img.filepath = tpath img.reload() - img.colorspace_settings.name = 'Linear' + img.colorspace_settings.name = 'sRGB' elif force_reload: if img.packed_file is not None: img.unpack(method='USE_ORIGINAL') img.reload() - img.colorspace_settings.name = 'Linear' + img.colorspace_settings.name = 'sRGB' return img @@ -304,7 +304,7 @@ def get_thumbnail(name): img = bpy.data.images.get(name) if img == None: img = bpy.data.images.load(p) - img.colorspace_settings.name = 'Linear' + img.colorspace_settings.name = 'sRGB' img.name = name img.name = name -- cgit v1.2.3 From 6f7c4230af8e404d9fa9687b80ee8e7f3f8eac1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Thu, 9 Jul 2020 09:00:56 +0200 Subject: BlenderKit: fix T70890 this is a complex fix that required to change some basic behaviours. - the temp folder from blenderkit_data was moved to system temp folder - There's a cleanup function for the old folder. - search itself doesn't create files on drive(this rework will continue to try not to write also some other data, like gravatars, but cache them in mem only) further fixes: - assetbar woudln't draw if there wasn't a thumbnail in an asset - categories panel poll function was fixed for brushes - fetching tokens from rerequests wasn't writing them into prefs TODO: create a popup that asks if categories can be downloaded and an example search can be performed. --- blenderkit/__init__.py | 19 +- blenderkit/bkit_oauth.py | 5 +- blenderkit/data/categories.json | 2193 ++++++++++----------------------------- blenderkit/paths.py | 21 +- blenderkit/rerequests.py | 2 + blenderkit/search.py | 99 +- blenderkit/ui.py | 16 +- blenderkit/ui_panels.py | 29 +- blenderkit/utils.py | 4 +- 9 files changed, 673 insertions(+), 1715 deletions(-) diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index d511bbf8..15dcaffb 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -19,8 +19,8 @@ bl_info = { "name": "BlenderKit Online Asset Library", "author": "Vilem Duha, Petr Dlouhy", - "version": (1, 0, 30), - "blender": (2, 82, 0), + "version": (1, 0, 31), + "blender": (2, 83, 0), "location": "View3D > Properties > BlenderKit", "description": "Online BlenderKit library (materials, models, brushes and more). Connects to the internet.", "warning": "", @@ -249,17 +249,24 @@ def switch_search_results(self, context): s['search results orig'] = s.get('bkit brush search orig') search.load_previews() +#define these static +# asset_type_ui_items = ( +# ('MODEL', 'Models', 'Find upload models in the BlenderKit online database', 'OBJECT_DATAMODE',0), +# # ('SCENE', 'SCENE', 'Browse scenes', 'SCENE_DATA', 1), +# ('MATERIAL', 'Materials', 'Find or upload models in the BlenderKit online database', 'MATERIAL',2), +# # ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3), +# ('BRUSH', 'Brushes', 'Find or upload models in the BlenderKit online database', 'BRUSH_DATA',3) +# ) +#same as above, but dynamic. def asset_type_callback(self, context): - # s = bpy.context.scene - # ui_props = s.blenderkitUI if self.down_up == 'SEARCH': items = ( ('MODEL', 'Models', 'Find models in the BlenderKit online database', 'OBJECT_DATAMODE', 0), # ('SCENE', 'SCENE', 'Browse scenes', 'SCENE_DATA', 1), - ('MATERIAL', 'Materials', 'Find models in the BlenderKit online database', 'MATERIAL', 2), + ('MATERIAL', 'Materials', 'Find materials in the BlenderKit online database', 'MATERIAL', 2), # ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3), - ('BRUSH', 'Brushes', 'Find models in the BlenderKit online database', 'BRUSH_DATA', 3) + ('BRUSH', 'Brushes', 'Find brushes in the BlenderKit online database', 'BRUSH_DATA', 3) ) else: items = ( diff --git a/blenderkit/bkit_oauth.py b/blenderkit/bkit_oauth.py index 4d2f09dc..0bf20d4a 100644 --- a/blenderkit/bkit_oauth.py +++ b/blenderkit/bkit_oauth.py @@ -70,7 +70,7 @@ def refresh_token_thread(): thread = threading.Thread(target=refresh_token, args=([preferences.api_key_refresh, url]), daemon=True) thread.start() else: - ui.add_report('Already Refreshing token, will be ready soon.') + ui.add_report('Already Refreshing token, will be ready soon. If this fails, please login again in Login panel.') def refresh_token(api_key_refresh, url): @@ -139,7 +139,8 @@ class Logout(bpy.types.Operator): preferences.login_attempt = False preferences.api_key_refresh = '' preferences.api_key = '' - del (bpy.context.window_manager['bkit profile']) + if bpy.context.window_manager.get('bkit profile'): + del (bpy.context.window_manager['bkit profile']) return {'FINISHED'} diff --git a/blenderkit/data/categories.json b/blenderkit/data/categories.json index 2eb34a34..d6286050 100644 --- a/blenderkit/data/categories.json +++ b/blenderkit/data/categories.json @@ -1,21 +1,4 @@ [ - { - "name": "addon", - "slug": "addon", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "addon", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, { "name": "brush", "slug": "brush", @@ -98,23 +81,6 @@ "assetCount": 8, "assetCountCumulative": 8 }, - { - "name": "crack", - "slug": "crack", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "crack", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, { "name": "cut", "slug": "cut", @@ -268,23 +234,6 @@ "assetCount": 1, "assetCountCumulative": 1 }, - { - "name": "nature", - "slug": "nature-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "nature", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, { "name": "pattern", "slug": "pattern", @@ -316,42 +265,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 7, - "assetCountCumulative": 7 - }, - { - "name": "rust", - "slug": "rust-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "rust", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "sculpture", - "slug": "sculpture-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "sculpture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 8, + "assetCountCumulative": 8 }, { "name": "stitches", @@ -370,23 +285,6 @@ "assetCount": 12, "assetCountCumulative": 12 }, - { - "name": "stone", - "slug": "stone-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "stone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, { "name": "tree", "slug": "tree-brush", @@ -403,23 +301,6 @@ "children": [], "assetCount": 4, "assetCountCumulative": 4 - }, - { - "name": "wood", - "slug": "wood-brush", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "wood", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 } ], "assetCount": 94, @@ -453,8 +334,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 2, - "assetCountCumulative": 2 + "assetCount": 36, + "assetCountCumulative": 36 }, { "name": "asphalt", @@ -470,8 +351,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 5, - "assetCountCumulative": 5 + "assetCount": 36, + "assetCountCumulative": 36 }, { "name": "bricks", @@ -487,8 +368,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 10, - "assetCountCumulative": 10 + "assetCount": 64, + "assetCountCumulative": 64 }, { "name": "ceramic", @@ -504,8 +385,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 2, - "assetCountCumulative": 2 + "assetCount": 16, + "assetCountCumulative": 16 }, { "name": "concrete", @@ -521,8 +402,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 21, - "assetCountCumulative": 21 + "assetCount": 64, + "assetCountCumulative": 64 }, { "name": "dirt", @@ -538,8 +419,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 6, - "assetCountCumulative": 6 + "assetCount": 25, + "assetCountCumulative": 25 }, { "name": "fabric", @@ -555,8 +436,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 41, - "assetCountCumulative": 41 + "assetCount": 169, + "assetCountCumulative": 169 }, { "name": "floor", @@ -572,8 +453,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 27, - "assetCountCumulative": 27 + "assetCount": 41, + "assetCountCumulative": 41 }, { "name": "food", @@ -589,8 +470,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 2, - "assetCountCumulative": 2 + "assetCount": 30, + "assetCountCumulative": 30 }, { "name": "fx", @@ -606,8 +487,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 19, + "assetCountCumulative": 19 }, { "name": "glass", @@ -623,25 +504,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 9, - "assetCountCumulative": 9 - }, - { - "name": "grass", - "slug": "grass", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "grass", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 28, + "assetCountCumulative": 28 }, { "name": "ground", @@ -657,25 +521,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 12, - "assetCountCumulative": 12 - }, - { - "name": "human", - "slug": "human", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "human", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 70, + "assetCountCumulative": 70 }, { "name": "ice", @@ -691,8 +538,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 2, - "assetCountCumulative": 2 + "assetCount": 20, + "assetCountCumulative": 20 }, { "name": "leather", @@ -708,8 +555,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 6, - "assetCountCumulative": 6 + "assetCount": 37, + "assetCountCumulative": 37 }, { "name": "liquid", @@ -725,25 +572,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 5, - "assetCountCumulative": 5 - }, - { - "name": "marble", - "slug": "marble", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "marble", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 7, + "assetCountCumulative": 7 }, { "name": "metal", @@ -759,8 +589,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 37, - "assetCountCumulative": 37 + "assetCount": 119, + "assetCountCumulative": 119 }, { "name": "organic", @@ -776,8 +606,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 3, - "assetCountCumulative": 3 + "assetCount": 29, + "assetCountCumulative": 29 }, { "name": "ornaments", @@ -793,8 +623,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 3, + "assetCountCumulative": 3 }, { "name": "paper", @@ -810,8 +640,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 26, - "assetCountCumulative": 26 + "assetCount": 30, + "assetCountCumulative": 30 }, { "name": "paving", @@ -827,8 +657,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 9, - "assetCountCumulative": 9 + "assetCount": 32, + "assetCountCumulative": 32 }, { "name": "plaster", @@ -844,8 +674,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 8, - "assetCountCumulative": 8 + "assetCount": 34, + "assetCountCumulative": 34 }, { "name": "plastic", @@ -861,8 +691,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 3, - "assetCountCumulative": 3 + "assetCount": 25, + "assetCountCumulative": 25 }, { "name": "rock", @@ -878,8 +708,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 7, - "assetCountCumulative": 7 + "assetCount": 21, + "assetCountCumulative": 21 }, { "name": "roofing", @@ -895,8 +725,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 2, - "assetCountCumulative": 2 + "assetCount": 12, + "assetCountCumulative": 12 }, { "name": "rubber", @@ -912,8 +742,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 8, + "assetCountCumulative": 8 }, { "name": "rust", @@ -929,8 +759,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 5, - "assetCountCumulative": 5 + "assetCount": 17, + "assetCountCumulative": 17 }, { "name": "sand", @@ -946,25 +776,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "soil", - "slug": "soil", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "soil", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 16, + "assetCountCumulative": 16 }, { "name": "stone", @@ -980,8 +793,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 11, - "assetCountCumulative": 11 + "assetCount": 117, + "assetCountCumulative": 117 }, { "name": "tech", @@ -997,8 +810,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 3, - "assetCountCumulative": 3 + "assetCount": 34, + "assetCountCumulative": 34 }, { "name": "tiles", @@ -1014,8 +827,8 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 19, - "assetCountCumulative": 19 + "assetCount": 84, + "assetCountCumulative": 84 }, { "name": "wood", @@ -1031,12 +844,12 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 42, - "assetCountCumulative": 42 + "assetCount": 121, + "assetCountCumulative": 121 } ], - "assetCount": 331, - "assetCountCumulative": 331 + "assetCount": 1365, + "assetCountCumulative": 1365 }, { "name": "model", @@ -1080,190 +893,174 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, + "assetCount": 20, + "assetCountCumulative": 20 + } + ], + "assetCount": 20, + "assetCountCumulative": 20 + }, + { + "name": "architecture", + "slug": "architecture", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "Architecture", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [ { - "name": "helicopter", - "slug": "helicopter", + "name": "elements", + "slug": "elements", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "helicopter", + "alternateTitle": "elements", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, + "assetCount": 133, + "assetCountCumulative": 133 + } + ], + "assetCount": 137, + "assetCountCumulative": 137 + }, + { + "name": "art", + "slug": "art", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "art", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [ { - "name": "historic", - "slug": "historic-aircraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "historic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "jet", - "slug": "jet", + "name": "literature", + "slug": "literature", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "jet", + "alternateTitle": "literature", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 9, + "assetCountCumulative": 9 }, { - "name": "part", - "slug": "part-aircraft", + "name": "painting", + "slug": "painting", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "part", + "alternateTitle": "painting", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 15, + "assetCountCumulative": 15 }, { - "name": "private", - "slug": "private", + "name": "sculpture", + "slug": "sculpture", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "private", + "alternateTitle": "sculpture", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "architecture", - "slug": "architecture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Architecture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ + "assetCount": 15, + "assetCountCumulative": 15 + }, { - "name": "elements", - "slug": "elements", + "name": "supplies", + "slug": "supplies", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "elements", + "alternateTitle": "supplies", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 4, - "assetCountCumulative": 4 + "assetCount": 8, + "assetCountCumulative": 8 } ], - "assetCount": 5, - "assetCountCumulative": 5 + "assetCount": 49, + "assetCountCumulative": 49 }, { - "name": "art", - "slug": "art", + "name": "character", + "slug": "character", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "art", + "alternateTitle": "character", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [ { - "name": "design", - "slug": "design", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "design", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "drawing", - "slug": "drawing", + "name": "anatomy", + "slug": "anatomy", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "drawing", + "alternateTitle": "anatomy", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 14, + "assetCountCumulative": 14 }, { - "name": "literature", - "slug": "literature", + "name": "clothing", + "slug": "clothing", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "literature", + "alternateTitle": "clothing", "alternateUrl": "", "description": "", "metaKeywords": "", @@ -1273,202 +1070,220 @@ "assetCountCumulative": 9 }, { - "name": "painting", - "slug": "painting", + "name": "fantasy", + "slug": "fantasy", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "painting", + "alternateTitle": "fantasy", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 8, - "assetCountCumulative": 8 + "assetCount": 2, + "assetCountCumulative": 2 }, { - "name": "sculpture", - "slug": "sculpture", + "name": "man", + "slug": "man", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "sculpture", + "alternateTitle": "man", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 8, - "assetCountCumulative": 8 + "assetCount": 13, + "assetCountCumulative": 13 }, { - "name": "supplies", - "slug": "supplies", + "name": "woman", + "slug": "woman", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "supplies", + "alternateTitle": "woman", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 7, + "assetCountCumulative": 7 } ], - "assetCount": 26, - "assetCountCumulative": 26 + "assetCount": 46, + "assetCountCumulative": 46 }, { - "name": "character", - "slug": "character", + "name": "exterior", + "slug": "exterior", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "character", + "alternateTitle": "exterior", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [ { - "name": "anatomy", - "slug": "anatomy", + "name": "building", + "slug": "building", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "anatomy", + "alternateTitle": "building", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 4, - "assetCountCumulative": 4 + "assetCount": 24, + "assetCountCumulative": 24 }, { - "name": "child", - "slug": "child", + "name": "cityspace", + "slug": "cityspace", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "child", + "alternateTitle": "cityspace", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 24, + "assetCountCumulative": 24 }, { - "name": "clothing", - "slug": "clothing", + "name": "landscape", + "slug": "landscape", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "clothing", + "alternateTitle": "landscape", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 4, - "assetCountCumulative": 4 + "assetCount": 35, + "assetCountCumulative": 35 }, { - "name": "fantasy", - "slug": "fantasy", + "name": "public", + "slug": "public", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "fantasy", + "alternateTitle": "public", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 12, + "assetCountCumulative": 12 }, { - "name": "man", - "slug": "man", + "name": "street", + "slug": "street", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "man", + "alternateTitle": "street", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 7, - "assetCountCumulative": 7 - }, + "assetCount": 35, + "assetCountCumulative": 35 + } + ], + "assetCount": 132, + "assetCountCumulative": 132 + }, + { + "name": "food & drink", + "slug": "food-drink", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "food & drink", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [ { - "name": "people", - "slug": "people", + "name": "container", + "slug": "container", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "people", + "alternateTitle": "container", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 33, + "assetCountCumulative": 33 }, { - "name": "sci-fi", - "slug": "sci-fi-character", + "name": "drink", + "slug": "drink", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "sci-fi", + "alternateTitle": "drink", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 27, + "assetCountCumulative": 27 }, { - "name": "woman", - "slug": "woman", + "name": "drugs", + "slug": "drugs", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "woman", + "alternateTitle": "drugs", "alternateUrl": "", "description": "", "metaKeywords": "", @@ -1476,225 +1291,225 @@ "children": [], "assetCount": 7, "assetCountCumulative": 7 + }, + { + "name": "food", + "slug": "food", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "food", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [], + "assetCount": 47, + "assetCountCumulative": 47 } ], - "assetCount": 22, - "assetCountCumulative": 22 + "assetCount": 117, + "assetCountCumulative": 117 }, { - "name": "exterior", - "slug": "exterior", + "name": "furniture", + "slug": "furniture", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "exterior", + "alternateTitle": "furniture", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [ { - "name": "building", - "slug": "building", + "name": "bed", + "slug": "bed", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "building", + "alternateTitle": "bed", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 4, - "assetCountCumulative": 4 + "assetCount": 21, + "assetCountCumulative": 21 }, { - "name": "cityspace", - "slug": "cityspace", + "name": "carpet", + "slug": "carpet", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "cityspace", + "alternateTitle": "carpet", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 6, - "assetCountCumulative": 6 + "assetCount": 10, + "assetCountCumulative": 10 }, { - "name": "historic", - "slug": "historic", + "name": "desk", + "slug": "desk", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "historic", + "alternateTitle": "desk", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 21, + "assetCountCumulative": 21 }, { - "name": "house", - "slug": "house", + "name": "fireplace", + "slug": "fireplace", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "house", + "alternateTitle": "fireplace", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 8, + "assetCountCumulative": 8 }, { - "name": "industrial", - "slug": "industrial-exterior", + "name": "lighting", + "slug": "lighting", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "industrial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "landmark", - "slug": "landmark", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "landmark", + "alternateTitle": "lighting", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 106, + "assetCountCumulative": 106 }, { - "name": "landscape", - "slug": "landscape", + "name": "seating", + "slug": "seating", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "landscape", + "alternateTitle": "seating", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 12, - "assetCountCumulative": 12 + "assetCount": 179, + "assetCountCumulative": 179 }, { - "name": "public", - "slug": "public", + "name": "shelving", + "slug": "shelving", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "public", + "alternateTitle": "shelving", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 4, - "assetCountCumulative": 4 + "assetCount": 38, + "assetCountCumulative": 38 }, { - "name": "sci-fi", - "slug": "sci-fi", + "name": "sofa", + "slug": "sofa", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "sci-fi", + "alternateTitle": "sofa", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 57, + "assetCountCumulative": 57 }, { - "name": "stadium", - "slug": "stadium", + "name": "storage", + "slug": "storage", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "stadium", + "alternateTitle": "storage", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 65, + "assetCountCumulative": 65 }, { - "name": "street", - "slug": "street", + "name": "table", + "slug": "table", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "street", + "alternateTitle": "table", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 106, + "assetCountCumulative": 106 } ], - "assetCount": 27, - "assetCountCumulative": 27 + "assetCount": 611, + "assetCountCumulative": 611 }, { - "name": "food & drink", - "slug": "food-drink", + "name": "industrial", + "slug": "industrial", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "food & drink", + "alternateTitle": "industrial", "alternateUrl": "", "description": "", "metaKeywords": "", @@ -1702,7 +1517,7 @@ "children": [ { "name": "container", - "slug": "container", + "slug": "container-industrial", "active": true, "thumbnail": null, "thumbnailWidth": null, @@ -1714,1592 +1529,720 @@ "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 10, - "assetCountCumulative": 10 + "assetCount": 48, + "assetCountCumulative": 48 }, { - "name": "drink", - "slug": "drink", + "name": "tool", + "slug": "tool", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "drink", + "alternateTitle": "tool", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - }, + "assetCount": 29, + "assetCountCumulative": 29 + } + ], + "assetCount": 85, + "assetCountCumulative": 85 + }, + { + "name": "interior", + "slug": "interior", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "interior", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [ { - "name": "drugs", - "slug": "drugs", + "name": "bathroom", + "slug": "bathroom", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "drugs", + "alternateTitle": "bathroom", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 3, - "assetCountCumulative": 3 + "assetCount": 96, + "assetCountCumulative": 96 }, { - "name": "food", - "slug": "food", + "name": "bedroom", + "slug": "bedroom", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "food", + "alternateTitle": "bedroom", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - } - ], - "assetCount": 35, - "assetCountCumulative": 35 - }, - { - "name": "furniture", - "slug": "furniture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "furniture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ + "assetCount": 64, + "assetCountCumulative": 64 + }, { - "name": "bed", - "slug": "bed", + "name": "decoration", + "slug": "decoration", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "bed", + "alternateTitle": "decoration", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 3, - "assetCountCumulative": 3 + "assetCount": 291, + "assetCountCumulative": 291 }, { - "name": "carpet", - "slug": "carpet", + "name": "hall", + "slug": "hall", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "carpet", + "alternateTitle": "hall", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 7, - "assetCountCumulative": 7 + "assetCount": 8, + "assetCountCumulative": 8 }, { - "name": "desk", - "slug": "desk", + "name": "kids room", + "slug": "kids-room", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "desk", + "alternateTitle": "kids room", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 5, - "assetCountCumulative": 5 + "assetCount": 42, + "assetCountCumulative": 42 }, { - "name": "fireplace", - "slug": "fireplace", + "name": "kitchen", + "slug": "kitchen", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "fireplace", + "alternateTitle": "kitchen", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 181, + "assetCountCumulative": 181 }, { - "name": "lighting", - "slug": "lighting", + "name": "living room", + "slug": "living-room", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "lighting", + "alternateTitle": "living room", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 19, - "assetCountCumulative": 19 + "assetCount": 127, + "assetCountCumulative": 127 }, { - "name": "seating", - "slug": "seating", + "name": "office", + "slug": "office", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "seating", + "alternateTitle": "office", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 36, - "assetCountCumulative": 36 + "assetCount": 48, + "assetCountCumulative": 48 }, { - "name": "shelving", - "slug": "shelving", + "name": "utility", + "slug": "utility", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "shelving", + "alternateTitle": "part", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 17, - "assetCountCumulative": 17 - }, + "assetCount": 74, + "assetCountCumulative": 74 + } + ], + "assetCount": 932, + "assetCountCumulative": 932 + }, + { + "name": "military", + "slug": "military", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "military", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [ { - "name": "sofa", - "slug": "sofa", + "name": "equipment", + "slug": "equipment", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "sofa", + "alternateTitle": "equipment", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 9, - "assetCountCumulative": 9 + "assetCount": 3, + "assetCountCumulative": 3 }, { - "name": "storage", - "slug": "storage", + "name": "historic", + "slug": "historic-military", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "storage", + "alternateTitle": "historic", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 24, - "assetCountCumulative": 24 + "assetCount": 6, + "assetCountCumulative": 6 }, { - "name": "table", - "slug": "table", + "name": "weapon", + "slug": "weapon", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "table", + "alternateTitle": "weapon", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 36, - "assetCountCumulative": 36 + "assetCount": 29, + "assetCountCumulative": 29 } ], - "assetCount": 157, - "assetCountCumulative": 157 + "assetCount": 40, + "assetCountCumulative": 40 }, { - "name": "industrial", - "slug": "industrial", + "name": "music", + "slug": "music", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "industrial", + "alternateTitle": "music", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [ { - "name": "agriculture", - "slug": "agriculture", + "name": "accessories", + "slug": "accessories", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "agriculture", + "alternateTitle": "accessories", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, + "assetCount": 13, + "assetCountCumulative": 13 + } + ], + "assetCount": 13, + "assetCountCumulative": 13 + }, + { + "name": "nature", + "slug": "nature", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "nature", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [ { - "name": "communication", - "slug": "communication", + "name": "animal", + "slug": "animal-nature", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "communication", + "alternateTitle": "animal", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 7, + "assetCountCumulative": 7 }, { - "name": "construction", - "slug": "construction", + "name": "atmosphere", + "slug": "atmosphere", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "construction", + "alternateTitle": "atmosphere", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 2, + "assetCountCumulative": 2 }, { - "name": "container", - "slug": "container-industrial", + "name": "landscape", + "slug": "landscape-nature", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "container", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 11, - "assetCountCumulative": 11 - }, - { - "name": "machine", - "slug": "machine", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "machine", + "alternateTitle": "landscape", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 35, + "assetCountCumulative": 35 }, { - "name": "tool", - "slug": "tool", + "name": "plant", + "slug": "plant", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "tool", + "alternateTitle": "plant", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 5, - "assetCountCumulative": 5 + "assetCount": 48, + "assetCountCumulative": 48 }, { - "name": "utility", - "slug": "utility-industrial", + "name": "tree", + "slug": "tree", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "part", + "alternateTitle": "tree", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 47, + "assetCountCumulative": 47 } ], - "assetCount": 18, - "assetCountCumulative": 18 + "assetCount": 141, + "assetCountCumulative": 141 }, { - "name": "interior", - "slug": "interior", + "name": "space", + "slug": "space", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "interior", + "alternateTitle": "space", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [ { - "name": "bathroom", - "slug": "bathroom", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "bathroom", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 24, - "assetCountCumulative": 24 - }, - { - "name": "bedroom", - "slug": "bedroom", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "bedroom", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - }, - { - "name": "decoration", - "slug": "decoration", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "decoration", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 71, - "assetCountCumulative": 71 - }, - { - "name": "hall", - "slug": "hall", + "name": "spacecraft", + "slug": "spacecraft", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "hall", + "alternateTitle": "spacecraft", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, + "assetCount": 9, + "assetCountCumulative": 9 + } + ], + "assetCount": 10, + "assetCountCumulative": 10 + }, + { + "name": "sports", + "slug": "sports", + "active": true, + "thumbnail": null, + "thumbnailWidth": null, + "thumbnailHeight": null, + "order": 0, + "alternateTitle": "sports", + "alternateUrl": "", + "description": "", + "metaKeywords": "", + "metaExtra": "", + "children": [ { - "name": "kids room", - "slug": "kids-room", + "name": "exercise", + "slug": "exercise", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "kids room", + "alternateTitle": "exercise", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 9, - "assetCountCumulative": 9 + "assetCount": 12, + "assetCountCumulative": 12 }, { - "name": "kitchen", - "slug": "kitchen", + "name": "extreme", + "slug": "extreme", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "kitchen", + "alternateTitle": "extreme", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 69, - "assetCountCumulative": 69 + "assetCount": 4, + "assetCountCumulative": 4 }, { - "name": "living room", - "slug": "living-room", + "name": "individual", + "slug": "individual", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "living room", + "alternateTitle": "individual", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 17, - "assetCountCumulative": 17 + "assetCount": 5, + "assetCountCumulative": 5 }, { - "name": "office", - "slug": "office", + "name": "outdoor", + "slug": "outdoor", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "office", + "alternateTitle": "outdoor", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 13, - "assetCountCumulative": 13 + "assetCount": 4, + "assetCountCumulative": 4 }, { - "name": "utility", - "slug": "utility", + "name": "team", + "slug": "team", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "part", + "alternateTitle": "team", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 34, - "assetCountCumulative": 34 + "assetCount": 6, + "assetCountCumulative": 6 } ], - "assetCount": 253, - "assetCountCumulative": 253 + "assetCount": 31, + "assetCountCumulative": 31 }, { - "name": "military", - "slug": "military", + "name": "technology", + "slug": "technology", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "military", + "alternateTitle": "technology", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [ { - "name": "air", - "slug": "air", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "air", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "equipment", - "slug": "equipment", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "equipment", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "ground", - "slug": "ground", + "name": "audio", + "slug": "audio", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "ground", + "alternateTitle": "audio", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 16, + "assetCountCumulative": 16 }, { - "name": "historic", - "slug": "historic-military", + "name": "computer", + "slug": "computer", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "historic", + "alternateTitle": "computer", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 102, + "assetCountCumulative": 102 }, { - "name": "naval", - "slug": "naval", + "name": "phone", + "slug": "phone", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "naval", + "alternateTitle": "phone", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 8, + "assetCountCumulative": 8 }, { - "name": "weapon", - "slug": "weapon", + "name": "photography", + "slug": "photography", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "weapon", + "alternateTitle": "photography", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 3, - "assetCountCumulative": 3 + "assetCount": 6, + "assetCountCumulative": 6 } ], - "assetCount": 3, - "assetCountCumulative": 3 + "assetCount": 138, + "assetCountCumulative": 138 }, { - "name": "music", - "slug": "music", + "name": "vehicle", + "slug": "vehicle", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "music", + "alternateTitle": "vehicle", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [ { - "name": "accessories", - "slug": "accessories", + "name": "car", + "slug": "car", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "accessories", + "alternateTitle": "car", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 43, + "assetCountCumulative": 43 }, { - "name": "instruments", - "slug": "instruments", + "name": "industrial", + "slug": "industrial-vehicle", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "instruments", + "alternateTitle": "industrial", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 5, + "assetCountCumulative": 5 }, { - "name": "stage", - "slug": "stage", + "name": "part", + "slug": "part-vehicle", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "stage", + "alternateTitle": "part", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 20, + "assetCountCumulative": 20 }, { - "name": "studio", - "slug": "studio", + "name": "Animals", + "slug": "animals", "active": true, "thumbnail": null, "thumbnailWidth": null, "thumbnailHeight": null, "order": 0, - "alternateTitle": "studio", + "alternateTitle": "Animals", "alternateUrl": "", "description": "", "metaKeywords": "", "metaExtra": "", "children": [], - "assetCount": 1, - "assetCountCumulative": 1 + "assetCount": 0, + "assetCountCumulative": 11 } ], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "nature", - "slug": "nature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "nature", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "animal", - "slug": "animal-nature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "animal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - }, - { - "name": "atmosphere", - "slug": "atmosphere", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "atmosphere", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "landscape", - "slug": "landscape-nature", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "landscape", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 9, - "assetCountCumulative": 9 - }, - { - "name": "plant", - "slug": "plant", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "plant", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 15, - "assetCountCumulative": 15 - }, - { - "name": "tree", - "slug": "tree", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "tree", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 14, - "assetCountCumulative": 14 - }, - { - "name": "weather", - "slug": "weather", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "weather", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 41, - "assetCountCumulative": 41 - }, - { - "name": "space", - "slug": "space", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "space", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "astronomy", - "slug": "astronomy", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "astronomy", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "planets", - "slug": "planets", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "planets", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "sci-fi", - "slug": "sci-fi-space", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "sci-fi", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "spacecraft", - "slug": "spacecraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "spacecraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - } - ], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "sports", - "slug": "sports", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "sports", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "animal", - "slug": "animal", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "animal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "exercise", - "slug": "exercise", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "exercise", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "extreme", - "slug": "extreme", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "extreme", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "individual", - "slug": "individual", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "individual", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "outdoor", - "slug": "outdoor", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "outdoor", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "team", - "slug": "team", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "team", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 3, - "assetCountCumulative": 3 - } - ], - "assetCount": 7, - "assetCountCumulative": 7 - }, - { - "name": "technology", - "slug": "technology", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "technology", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "ai", - "slug": "ai", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "ai", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "audio", - "slug": "audio", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "audio", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "computer", - "slug": "computer", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "computer", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "medical", - "slug": "medical", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "medical", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "phone", - "slug": "phone", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "phone", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 2, - "assetCountCumulative": 2 - }, - { - "name": "photography", - "slug": "photography", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "photography", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "science", - "slug": "science", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "science", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "video", - "slug": "video", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "video", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 6, - "assetCountCumulative": 6 - }, - { - "name": "vehicle", - "slug": "vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "vehicle", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "bicycle", - "slug": "bicycle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "bicycle", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "bus", - "slug": "bus", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "bus", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "car", - "slug": "car", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "car", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 9, - "assetCountCumulative": 9 - }, - { - "name": "historic", - "slug": "historic-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "historic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "industrial", - "slug": "industrial-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "industrial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 1, - "assetCountCumulative": 1 - }, - { - "name": "motorcycle", - "slug": "motorcycle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "motorcycle", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "part", - "slug": "part-vehicle", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "part", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 8, - "assetCountCumulative": 8 - }, - { - "name": "train", - "slug": "train", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "train", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "truck", - "slug": "truck", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "truck", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 18, - "assetCountCumulative": 18 - }, - { - "name": "watercraft", - "slug": "watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "watercraft", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "historic", - "slug": "historic-watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "historic", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "industrial", - "slug": "industrial-watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "industrial", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "part", - "slug": "part-watercraft", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "part", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "personal", - "slug": "personal", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "personal", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "recreational", - "slug": "recreational", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "recreational", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 621, - "assetCountCumulative": 621 - }, - { - "name": "texture", - "slug": "texture", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "texture", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Animals", - "slug": "animals", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Animals", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [ - { - "name": "Mammals", - "slug": "mammals", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Mammals", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - }, - { - "name": "Plants", - "slug": "plants", - "active": true, - "thumbnail": null, - "thumbnailWidth": null, - "thumbnailHeight": null, - "order": 0, - "alternateTitle": "Plants", - "alternateUrl": "", - "description": "", - "metaKeywords": "", - "metaExtra": "", - "children": [], - "assetCount": 0, - "assetCountCumulative": 0 - } - ], - "assetCount": 0, - "assetCountCumulative": 1 + "assetCount": 70, + "assetCountCumulative": 70 } ], - "assetCount": 0, - "assetCountCumulative": 0 + "assetCount": 2572, + "assetCountCumulative": 2572 } ] \ No newline at end of file diff --git a/blenderkit/paths.py b/blenderkit/paths.py index 2f144268..b4210a85 100644 --- a/blenderkit/paths.py +++ b/blenderkit/paths.py @@ -16,7 +16,8 @@ # # ##### END GPL LICENSE BLOCK ##### -import bpy, os, sys +import bpy, os, sys, tempfile, shutil +from blenderkit import tasks_queue, ui _presets = os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets") BLENDERKIT_LOCAL = "http://localhost:8001" @@ -35,6 +36,15 @@ BLENDERKIT_OAUTH_LANDING_URL = "/oauth-landing/" BLENDERKIT_SIGNUP_URL = "https://www.blenderkit.com/accounts/register" BLENDERKIT_SETTINGS_FILENAME = os.path.join(_presets, "bkit.json") +def cleanup_old_folders(): + '''function to clean up any historical folders for BlenderKit. By now removes the temp folder.''' + orig_temp = os.path.join(os.path.expanduser('~'), 'blenderkit_data', 'temp') + if os.path.isdir(orig_temp): + try: + shutil.rmtree(orig_temp) + except Exception as e: + print(e) + print("couldn't delete old temp directory") def get_bkit_url(): # bpy.app.debug_value = 2 @@ -81,7 +91,7 @@ def get_temp_dir(subdir=None): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences # tempdir = user_preferences.temp_dir - tempdir = os.path.join(user_preferences.global_dir, 'temp') + tempdir = os.path.join(tempfile.gettempdir(), 'bkit_temp') if tempdir.startswith('//'): tempdir = bpy.path.abspath(tempdir) try: @@ -91,11 +101,14 @@ def get_temp_dir(subdir=None): tempdir = os.path.join(tempdir, subdir) if not os.path.exists(tempdir): os.makedirs(tempdir) + cleanup_old_folders() except: - print('Cache directory not found. Resetting Cache folder path.') + tasks_queue.add_task((ui.add_report, ('Cache directory not found. Resetting Cache folder path.',))) + p = default_global_dict() if p == user_preferences.global_dir: - print('Global dir was already default, plese set a global directory in addon preferences to a dir where you have write permissions.') + message = 'Global dir was already default, plese set a global directory in addon preferences to a dir where you have write permissions.' + tasks_queue.add_task((ui.add_report, (message,))) return None user_preferences.global_dir = p tempdir = get_temp_dir(subdir = subdir) diff --git a/blenderkit/rerequests.py b/blenderkit/rerequests.py index 28c7e2ca..587093e0 100644 --- a/blenderkit/rerequests.py +++ b/blenderkit/rerequests.py @@ -68,6 +68,8 @@ def rerequest(method, url, **kwargs): # in non-threaded tasks bpy.context.preferences.addons['blenderkit'].preferences.api_key = auth_token bpy.context.preferences.addons['blenderkit'].preferences.api_key_refresh = refresh_token + else: + tasks_queue.add_task((bkit_oauth.write_tokens, (auth_token, refresh_token, oauth_response))) kwargs['headers'] = utils.get_headers(auth_token) response = requests.request(method, url, **kwargs) diff --git a/blenderkit/search.py b/blenderkit/search.py index d77784d9..bae25466 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -56,8 +56,8 @@ import requests, os, random import time import threading import platform -import json import bpy +import copy search_start_time = 0 prev_time = 0 @@ -140,7 +140,7 @@ def fetch_server_data(): api_key = user_preferences.api_key # Only refresh new type of tokens(by length), and only one hour before the token timeouts. if user_preferences.enable_oauth and \ - len(user_preferences.api_key) < 38 and \ + len(user_preferences.api_key) < 38 and len(user_preferences.api_key) > 0 and \ user_preferences.api_key_timeout < time.time() + 3600: bkit_oauth.refresh_token_thread() if api_key != '' and bpy.context.window_manager.get('bkit profile') == None: @@ -266,7 +266,7 @@ def parse_result(r): # attempt to switch to use original data gradually, since the parsing as itself should become obsolete. asset_data.update(r) - return asset_data + return asset_data # @bpy.app.handlers.persistent @@ -312,20 +312,17 @@ def timer_update(): asset_type = thread[2] if asset_type == 'model': props = scene.blenderkit_models - json_filepath = os.path.join(icons_dir, 'model_searchresult.json') - search_name = 'bkit model search' + # json_filepath = os.path.join(icons_dir, 'model_searchresult.json') if asset_type == 'scene': props = scene.blenderkit_scene - json_filepath = os.path.join(icons_dir, 'scene_searchresult.json') - search_name = 'bkit scene search' + # json_filepath = os.path.join(icons_dir, 'scene_searchresult.json') if asset_type == 'material': props = scene.blenderkit_mat - json_filepath = os.path.join(icons_dir, 'material_searchresult.json') - search_name = 'bkit material search' + # json_filepath = os.path.join(icons_dir, 'material_searchresult.json') if asset_type == 'brush': props = scene.blenderkit_brush - json_filepath = os.path.join(icons_dir, 'brush_searchresult.json') - search_name = 'bkit brush search' + # json_filepath = os.path.join(icons_dir, 'brush_searchresult.json') + search_name = f'bkit {asset_type} search' s[search_name] = [] @@ -333,8 +330,8 @@ def timer_update(): if reports != '': props.report = str(reports) return .2 - with open(json_filepath, 'r') as data_file: - rdata = json.load(data_file) + + rdata = thread[0].result result_field = [] ok, error = check_errors(rdata) @@ -348,8 +345,9 @@ def timer_update(): # results = rdata['results'] s[search_name] = result_field s['search results'] = result_field - s[search_name + ' orig'] = rdata - s['search results orig'] = rdata + s[search_name + ' orig'] = copy.deepcopy(rdata) + s['search results orig'] = s[search_name + ' orig'] + load_previews() ui_props = bpy.context.scene.blenderkitUI if len(result_field) < ui_props.scrolloffset: @@ -360,9 +358,6 @@ def timer_update(): if len(s['search results']) == 0: tasks_queue.add_task((ui.add_report, ('No matching results found.',))) - # (rdata['next']) - # if rdata['next'] != None: - # search(False, get_next = True) else: print('error', error) props.report = error @@ -374,18 +369,11 @@ def timer_update(): def load_previews(): - mappingdict = { - 'MODEL': 'model', - 'SCENE': 'scene', - 'MATERIAL': 'material', - 'TEXTURE': 'texture', - 'BRUSH': 'brush' - } + scene = bpy.context.scene # FIRST START SEARCH props = scene.blenderkitUI - - directory = paths.get_temp_dir('%s_search' % mappingdict[props.asset_type]) + directory = paths.get_temp_dir('%s_search' % props.asset_type.lower()) s = bpy.context.scene results = s.get('search results') # @@ -694,7 +682,7 @@ def write_gravatar(a_id, gravatar_path): def fetch_gravatar(adata): utils.p('fetch gravatar') if adata.get('gravatarHash') is not None: - gravatar_path = paths.get_temp_dir(subdir='g/') + adata['gravatarHash'] + '.jpg' + gravatar_path = paths.get_temp_dir(subdir='bkit_g/') + adata['gravatarHash'] + '.jpg' if os.path.exists(gravatar_path): tasks_queue.add_task((write_gravatar, (adata['id'], gravatar_path))) @@ -790,11 +778,12 @@ def get_profile(): class Searcher(threading.Thread): query = None - def __init__(self, query, params): + def __init__(self, query, params,orig_result): super(Searcher, self).__init__() self.query = query self.params = params self._stop_event = threading.Event() + self.result = orig_result def stop(self): self._stop_event.set() @@ -854,7 +843,7 @@ class Searcher(threading.Thread): t = time.time() mt('search thread started') tempdir = paths.get_temp_dir('%s_search' % query['asset_type']) - json_filepath = os.path.join(tempdir, '%s_searchresult.json' % query['asset_type']) + # json_filepath = os.path.join(tempdir, '%s_searchresult.json' % query['asset_type']) headers = utils.get_headers(params['api_key']) @@ -862,23 +851,11 @@ class Searcher(threading.Thread): rdata['results'] = [] if params['get_next']: - with open(json_filepath, 'r') as infile: - try: - origdata = json.load(infile) - urlquery = origdata['next'] - # rparameters = {} - if urlquery == None: - return; - except: - # in case no search results found on drive we don't do next page loading. - params['get_next'] = False + urlquery = self.result['next'] if not params['get_next']: - url = paths.get_api_url() + 'search/' + urlquery = self.query_to_url() - urlquery = url - # rparameters = query - urlquery = self.query_to_url() try: utils.p(urlquery) r = rerequests.get(urlquery, headers=headers) # , params = rparameters) @@ -941,10 +918,10 @@ class Searcher(threading.Thread): # we save here because a missing thumbnail check is in the previous loop # we can also prepend previous results. These have downloaded thumbnails already... if params['get_next']: - rdata['results'][0:0] = origdata['results'] - - with open(json_filepath, 'w') as outfile: - json.dump(rdata, outfile) + rdata['results'][0:0] = self.result['results'] + self.result = rdata + # with open(json_filepath, 'w') as outfile: + # json.dump(rdata, outfile) killthreads_sml = [] for k in thumb_sml_download_threads.keys(): @@ -1157,7 +1134,7 @@ def mt(text): utils.p(text, alltime, since_last) -def add_search_process(query, params): +def add_search_process(query, params, orig_result): global search_threads while (len(search_threads) > 0): @@ -1166,10 +1143,10 @@ def add_search_process(query, params): # TODO CARE HERE FOR ALSO KILLING THE THREADS...AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN OLDER ONE tempdir = paths.get_temp_dir('%s_search' % query['asset_type']) - thread = Searcher(query, params) + thread = Searcher(query, params, orig_result) thread.start() - search_threads.append([thread, tempdir, query['asset_type']]) + search_threads.append([thread, tempdir, query['asset_type'],{}])# 4th field is for results mt('thread started') @@ -1184,6 +1161,14 @@ def search(category='', get_next=False, author_id=''): scene = bpy.context.scene ui_props = scene.blenderkitUI + ### updating of search categories was moved here, due to the reason that BlenderKit created the blenderkit_data + # folder upon registration of BlenderKit, which wasn't a favourite option for some users (devs running tests). + # user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + # if not user_preferences.first_run: + # api_key = user_preferences.api_key + # if bpy.context.window_manager.get('bkit_categories') is None: + # categories.fetch_categories_thread(api_key) + if ui_props.asset_type == 'MODEL': if not hasattr(scene, 'blenderkit'): return; @@ -1195,13 +1180,14 @@ def search(category='', get_next=False, author_id=''): return; props = scene.blenderkit_scene query = build_query_scene() + if ui_props.asset_type == 'MATERIAL': if not hasattr(scene, 'blenderkit_mat'): return; props = scene.blenderkit_mat query = build_query_material() - utils.p(query) + if ui_props.asset_type == 'TEXTURE': if not hasattr(scene, 'blenderkit_tex'): @@ -1209,12 +1195,14 @@ def search(category='', get_next=False, author_id=''): # props = scene.blenderkit_tex # query = build_query_texture() + if ui_props.asset_type == 'BRUSH': if not hasattr(scene, 'blenderkit_brush'): return; props = scene.blenderkit_brush query = build_query_brush() + if props.is_searching and get_next == True: return; @@ -1242,8 +1230,11 @@ def search(category='', get_next=False, author_id=''): # if free_only: # query['keywords'] += '+is_free:true' - - add_search_process(query, params) + orig_results = scene.get(f'bkit {ui_props.asset_type.lower()} search orig', {}) + if orig_results != {}: + #ensure it's a copy in dict for what we are passing to thread: + orig_results = orig_results.to_dict() + add_search_process(query, params, orig_results) tasks_queue.add_task((ui.add_report, ('BlenderKit searching....', 2))) props.report = 'BlenderKit searching....' diff --git a/blenderkit/ui.py b/blenderkit/ui.py index f13b4778..a1cd66d9 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -796,14 +796,14 @@ def draw_callback_2d_search(self, context): index = a + ui_props.scrolloffset + b * ui_props.wcount iname = utils.previmg_name(index) img = bpy.data.images.get(iname) - - w = int(ui_props.thumb_size * img.size[0] / max(img.size[0], img.size[1])) - h = int(ui_props.thumb_size * img.size[1] / max(img.size[0], img.size[1])) - crop = (0, 0, 1, 1) - if img.size[0] > img.size[1]: - offset = (1 - img.size[1] / img.size[0]) / 2 - crop = (offset, 0, 1 - offset, 1) if img is not None: + w = int(ui_props.thumb_size * img.size[0] / max(img.size[0], img.size[1])) + h = int(ui_props.thumb_size * img.size[1] / max(img.size[0], img.size[1])) + crop = (0, 0, 1, 1) + if img.size[0] > img.size[1]: + offset = (1 - img.size[1] / img.size[0]) / 2 + crop = (offset, 0, 1 - offset, 1) + ui_bgl.draw_image(x, y, w, w, img, 1, crop=crop) if index == ui_props.active_index: @@ -815,7 +815,7 @@ def draw_callback_2d_search(self, context): # w + 2*highlight_margin, h + 2*highlight_margin , highlight) else: - ui_bgl.draw_rect(x, y, w, h, white) + ui_bgl.draw_rect(x, y, ui_props.thumb_size, ui_props.thumb_size, white) result = search_results[index] if result['downloaded'] > 0: diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index 7de2a240..1083b6a6 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -94,7 +94,7 @@ def draw_ratings(layout, context): # row = layout.row() # op = row.operator("object.blenderkit_rating_upload", text="Send rating", icon='URL') # return op - #re-enable layout if included in longer panel + # re-enable layout if included in longer panel def draw_not_logged_in(source): @@ -337,7 +337,6 @@ def draw_panel_model_search(self, context): # draw_panel_categories(self, context) - def draw_panel_scene_search(self, context): s = context.scene props = s.blenderkit_scene @@ -567,12 +566,9 @@ def draw_panel_material_search(self, context): # if props.search_engine == 'OTHER': # layout.prop(props, 'search_engine_other') - - # draw_panel_categories(self, context) - def draw_panel_material_ratings(self, context): draw_ratings(self.layout, context) # , props) # op.asset_type = 'MATERIAL' @@ -641,12 +637,11 @@ class VIEW3D_PT_blenderkit_advanced_model_search(Panel): bl_label = "Search filters" bl_options = {'DEFAULT_CLOSED'} - @classmethod def poll(cls, context): s = context.scene ui_props = s.blenderkitUI - return ui_props.down_up == 'SEARCH' and ui_props.asset_type =='MODEL' + return ui_props.down_up == 'SEARCH' and ui_props.asset_type == 'MODEL' def draw(self, context): s = context.scene @@ -696,6 +691,7 @@ class VIEW3D_PT_blenderkit_advanced_model_search(Panel): # ADULT # layout.prop(props, "search_adult") # , text ='condition of object new/old e.t.c.') + class VIEW3D_PT_blenderkit_advanced_material_search(Panel): bl_category = "BlenderKit" bl_idname = "VIEW3D_PT_blenderkit_advanced_material_search" @@ -709,7 +705,7 @@ class VIEW3D_PT_blenderkit_advanced_material_search(Panel): def poll(cls, context): s = context.scene ui_props = s.blenderkitUI - return ui_props.down_up == 'SEARCH' and ui_props.asset_type =='MATERIAL' + return ui_props.down_up == 'SEARCH' and ui_props.asset_type == 'MATERIAL' def draw(self, context): s = context.scene @@ -737,6 +733,7 @@ class VIEW3D_PT_blenderkit_advanced_material_search(Panel): row.prop(props, "search_file_size_min", text='min') row.prop(props, "search_file_size_max", text='max') + class VIEW3D_PT_blenderkit_categories(Panel): bl_category = "BlenderKit" bl_idname = "VIEW3D_PT_blenderkit_categories" @@ -750,10 +747,14 @@ class VIEW3D_PT_blenderkit_categories(Panel): def poll(cls, context): s = context.scene ui_props = s.blenderkitUI - return ui_props.down_up == 'SEARCH' + mode = True + if ui_props.asset_type == 'BRUSH' and not (context.sculpt_object or context.image_paint_object): + mode = False + return ui_props.down_up == 'SEARCH' and mode def draw(self, context): - draw_panel_categories(self,context) + draw_panel_categories(self, context) + class VIEW3D_PT_blenderkit_import_settings(Panel): bl_category = "BlenderKit" @@ -776,7 +777,6 @@ class VIEW3D_PT_blenderkit_import_settings(Panel): s = context.scene ui_props = s.blenderkitUI - if ui_props.asset_type == 'MODEL': # noinspection PyCallByClass props = s.blenderkit_models @@ -821,7 +821,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): # row = row.split().row() # layout.alert = True # layout.alignment = 'CENTER' - row = layout.row(align = True) + row = layout.row(align=True) row.scale_x = 1.6 row.scale_y = 1.6 # split = row.split(factor=.5) @@ -966,10 +966,10 @@ def draw_asset_context_menu(self, context, asset_data): aob = bpy.context.selected_objects[0] op = layout.operator('scene.blenderkit_download', text='Replace Active Models') - #this checks if the menu got called from right-click in assetbar(then index is 0 - x) or + # this checks if the menu got called from right-click in assetbar(then index is 0 - x) or # from a panel(then replacement happens from the active model) if ui_props.active_index == -3: - #called from addon panel + # called from addon panel o = utils.get_active_model() op.asset_base_id = o['asset_data']['assetBaseId'] else: @@ -1232,6 +1232,7 @@ classess = ( UrlPopupDialog ) + def register_ui_panels(): for c in classess: bpy.utils.register_class(c) diff --git a/blenderkit/utils.py b/blenderkit/utils.py index 5544ba3e..503a93f2 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -209,9 +209,9 @@ def get_upload_props(): def previmg_name(index, fullsize=False): if not fullsize: - return '.bkit_preview_' + str(index).zfill(2) + return '.bkit_preview_' + str(index).zfill(3) else: - return '.bkit_preview_full_' + str(index).zfill(2) + return '.bkit_preview_full_' + str(index).zfill(3) def get_active_brush(): -- cgit v1.2.3 From 054ee2f41734049c9ba1386d9ec9eb3e85fc2e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Sun, 12 Jul 2020 18:07:45 +0200 Subject: BlenderKit: fix rating interaction -disabled layout didn't work(thanks to another blender layout bug that I reported) -replaced it with an enum, and also a popup that informs the user instead. --- blenderkit/__init__.py | 76 ++++++++++++------ blenderkit/bkit_oauth.py | 21 ++++- blenderkit/ratings.py | 98 ++++++++++++++++-------- blenderkit/rerequests.py | 2 +- blenderkit/search.py | 2 +- blenderkit/ui_panels.py | 195 ++++++++++++++++++++++++++++++++--------------- blenderkit/upload.py | 2 +- blenderkit/utils.py | 38 +++++++++ 8 files changed, 311 insertions(+), 123 deletions(-) diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index 15dcaffb..a7e148ea 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -93,6 +93,7 @@ def scene_load(context): preferences = bpy.context.preferences.addons['blenderkit'].preferences preferences.login_attempt = False + @bpy.app.handlers.persistent def check_timers_timer(): ''' checks if all timers are registered regularly. Prevents possible bugs from stopping the addon.''' @@ -249,17 +250,12 @@ def switch_search_results(self, context): s['search results orig'] = s.get('bkit brush search orig') search.load_previews() -#define these static -# asset_type_ui_items = ( -# ('MODEL', 'Models', 'Find upload models in the BlenderKit online database', 'OBJECT_DATAMODE',0), -# # ('SCENE', 'SCENE', 'Browse scenes', 'SCENE_DATA', 1), -# ('MATERIAL', 'Materials', 'Find or upload models in the BlenderKit online database', 'MATERIAL',2), -# # ('TEXTURE', 'Texture', 'Browse textures', 'TEXTURE', 3), -# ('BRUSH', 'Brushes', 'Find or upload models in the BlenderKit online database', 'BRUSH_DATA',3) -# ) - -#same as above, but dynamic. def asset_type_callback(self, context): + ''' + Returns + items for Enum property, depending on the down_up property - BlenderKit is either in search or in upload mode. + + ''' if self.down_up == 'SEARCH': items = ( ('MODEL', 'Models', 'Find models in the BlenderKit online database', 'OBJECT_DATAMODE', 0), @@ -533,7 +529,7 @@ def update_free(self, context): "Part of subscription is sent to artists based on usage by paying users." def draw_message(self, context): - ui_panels.label_multiline(self.layout, text=message, icon='NONE', width=-1) + utils.label_multiline(self.layout, text=message, icon='NONE', width=-1) bpy.context.window_manager.popup_menu(draw_message, title=title, icon='INFO') @@ -654,6 +650,29 @@ class BlenderKitCommonUploadProps(object): ) +def stars_enum_callback(self, context): + items = [] + for a in range(0, 10): + if self.rating_quality < a+1: + icon = 'SOLO_OFF' + else: + icon = 'SOLO_ON' + # has to have something before the number in the value, otherwise fails on registration. + items.append((f'{a+1}', f'{a+1}', '', icon, a+1)) + return items + + +def update_quality(self, context): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + if user_preferences.api_key == '': + # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') + # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') + # return + bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', message = 'Please login/signup to rate assets. Clicking OK takes you to web login.') + self.rating_quality_ui = '0' + self.rating_quality = int(self.rating_quality_ui) + + class BlenderKitRatingProps(PropertyGroup): rating_quality: IntProperty(name="Quality", description="quality of the material", @@ -661,17 +680,25 @@ class BlenderKitRatingProps(PropertyGroup): min=-1, max=10, update=ratings.update_ratings_quality) + #the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. + rating_quality_ui: EnumProperty(name='rating_quality_ui', + items=stars_enum_callback, + description='Rating stars 0 - 10', + default=None, + update=update_quality, + ) + rating_work_hours: FloatProperty(name="Work Hours", description="How many hours did this work take?", default=0.00, min=0.0, max=1000, update=ratings.update_ratings_work_hours ) - rating_complexity: IntProperty(name="Complexity", - description="Complexity is a number estimating how much work was spent on the asset.aaa", - default=0, min=0, max=10) - rating_virtual_price: FloatProperty(name="Virtual Price", - description="How much would you pay for this object if buing it?", - default=0, min=0, max=10000) + # rating_complexity: IntProperty(name="Complexity", + # description="Complexity is a number estimating how much work was spent on the asset.aaa", + # default=0, min=0, max=10) + # rating_virtual_price: FloatProperty(name="Virtual Price", + # description="How much would you pay for this object if buing it?", + # default=0, min=0, max=10000) rating_problems: StringProperty( name="Problems", description="Problems found/ why did you take points down - this will be available for the author" @@ -1400,14 +1427,15 @@ class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps): update=search.search_update ) + def fix_subdir(self, context): '''Fixes project subdicrectory settings if people input invalid path.''' # pp = pathlib.PurePath(self.project_subdir) pp = self.project_subdir[:] - pp = pp.replace('\\','') - pp = pp.replace('/','') - pp = pp.replace(':','') + pp = pp.replace('\\', '') + pp = pp.replace('/', '') + pp = pp.replace(':', '') pp = '//' + pp if self.project_subdir != pp: self.project_subdir = pp @@ -1418,10 +1446,11 @@ def fix_subdir(self, context): "and uses it for storing assets." def draw_message(self, context): - ui_panels.label_multiline(self.layout, text=message, icon='NONE', width=400) + utils.label_multiline(self.layout, text=message, icon='NONE', width=400) bpy.context.window_manager.popup_menu(draw_message, title=title, icon='INFO') + class BlenderKitAddonPreferences(AddonPreferences): # this must match the addon name, use '__package__' # when defining this in a submodule of a python package. @@ -1501,7 +1530,7 @@ class BlenderKitAddonPreferences(AddonPreferences): description="where data will be stored for individual projects", # subtype='DIR_PATH', default="//assets", - update = fix_subdir + update=fix_subdir ) directory_behaviour: EnumProperty( @@ -1566,10 +1595,11 @@ class BlenderKitAddonPreferences(AddonPreferences): use_timers: BoolProperty( name="Use timers", - description="Use timers for bkit", + description="Use timers for BlenderKit. Usefull for debugging since timers seem to be unstable.", default=True, update=utils.save_prefs ) + # allow_proximity : BoolProperty( # name="allow proximity data reports", # description="This sends anonymized proximity data \n \ diff --git a/blenderkit/bkit_oauth.py b/blenderkit/bkit_oauth.py index 0bf20d4a..ae90b215 100644 --- a/blenderkit/bkit_oauth.py +++ b/blenderkit/bkit_oauth.py @@ -26,8 +26,9 @@ if "bpy" in locals(): categories = reload(categories) oauth = reload(oauth) ui = reload(ui) + ui = reload(ui_panels) else: - from blenderkit import tasks_queue, utils, paths, search, categories, oauth, ui + from blenderkit import tasks_queue, utils, paths, search, categories, oauth, ui, ui_panels import bpy @@ -102,7 +103,7 @@ class RegisterLoginOnline(bpy.types.Operator): """Login online on BlenderKit webpage""" bl_idname = "wm.blenderkit_login" - bl_label = "BlenderKit login or signup" + bl_label = "BlenderKit login/signup" bl_options = {'REGISTER', 'UNDO'} signup: BoolProperty( @@ -112,16 +113,32 @@ class RegisterLoginOnline(bpy.types.Operator): options={'SKIP_SAVE'} ) + message: bpy.props.StringProperty( + name="Message", + description="", + default="You were logged out from BlenderKit. Clicking OK takes you to web login. ") + @classmethod def poll(cls, context): return True + def draw(self, context): + layout = self.layout + utils.label_multiline(layout, text=self.message) + def execute(self, context): preferences = bpy.context.preferences.addons['blenderkit'].preferences preferences.login_attempt = True login_thread(self.signup) return {'FINISHED'} + def invoke(self, context, event): + wm = bpy.context.window_manager + preferences = bpy.context.preferences.addons['blenderkit'].preferences + preferences.api_key_refresh = '' + preferences.api_key = '' + return wm.invoke_props_dialog(self) + class Logout(bpy.types.Operator): """Logout from BlenderKit immediately""" diff --git a/blenderkit/ratings.py b/blenderkit/ratings.py index dbe8b8eb..48c34a61 100644 --- a/blenderkit/ratings.py +++ b/blenderkit/ratings.py @@ -78,6 +78,7 @@ def send_rating_to_thread_quality(url, ratings, headers): thread = threading.Thread(target=upload_rating_thread, args=(url, ratings, headers)) thread.start() + def send_rating_to_thread_work_hours(url, ratings, headers): '''Sens rating into thread rating, main purpose is for tasks_queue. One function per property to avoid lost data due to stashing.''' @@ -93,6 +94,7 @@ def upload_review_thread(url, reviews, headers): def get_rating(asset_id): + #this function isn't used anywhere,should probably get removed. user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key headers = utils.get_headers(api_key) @@ -133,6 +135,7 @@ def update_ratings_work_hours(self, context): tasks_queue.add_task((send_rating_to_thread_work_hours, (url, ratings, headers)), wait=1, only_last=True) + def upload_rating(asset): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key @@ -170,26 +173,43 @@ def upload_rating(asset): if bkit_ratings.rating_quality > 0.1 and bkit_ratings.rating_work_hours > 0.1: s['assets rated'][asset['asset_data']['assetBaseId']] = True - -class StarRatingOperator(bpy.types.Operator): - """Tooltip""" - bl_idname = "object.blenderkit_rating" - bl_label = "Rate the Asset Quality" - bl_options = {'REGISTER', 'INTERNAL'} - - property_name: StringProperty( - name="Rating Property", - description="Property that is rated", - default="", - ) - - rating: IntProperty(name="Rating", description="rating value", default=1, min=1, max=10) - - def execute(self, context): - asset = utils.get_active_asset() - props = asset.bkit_ratings - props.rating_quality = self.rating - return {'FINISHED'} +def get_assets_for_rating(): + ''' + gets assets from scene that could/should be rated by the user. + TODO this is only a draft. + + ''' + assets = [] + for ob in bpy.context.scene.objects: + if ob.get('asset_data'): + assets.append(ob) + for m in bpy.data.materials: + if m.get('asset_data'): + assets.append(m) + for b in bpy.data.brushes: + if b.get('asset_data'): + assets.append(b) + return assets + +# class StarRatingOperator(bpy.types.Operator): +# """Tooltip""" +# bl_idname = "object.blenderkit_rating" +# bl_label = "Rate the Asset Quality" +# bl_options = {'REGISTER', 'INTERNAL'} +# +# property_name: StringProperty( +# name="Rating Property", +# description="Property that is rated", +# default="", +# ) +# +# rating: IntProperty(name="Rating", description="rating value", default=1, min=1, max=10) +# +# def execute(self, context): +# asset = utils.get_active_asset() +# props = asset.bkit_ratings +# props.rating_quality = self.rating +# return {'FINISHED'} asset_types = ( @@ -234,29 +254,43 @@ class UploadRatingOperator(bpy.types.Operator): return wm.invoke_props_dialog(self) + def draw_rating(layout, props, prop_name, name): # layout.label(name) row = layout.row(align=True) - - for a in range(0, 10): - if eval('props.' + prop_name) < a + 1: - icon = 'SOLO_OFF' - else: - icon = 'SOLO_ON' - - op = row.operator('object.blenderkit_rating', icon=icon, emboss=False, text='') - op.property_name = prop_name - op.rating = a + 1 + # test method - 10 booleans. + # propsx = bpy.context.active_object.bkit_ratings + # for a in range(0, 10): + # pn = f'rq{str(a+1).zfill(2)}' + # if eval('propsx.' + pn) == False: + # icon = 'SOLO_OFF' + # else: + # icon = 'SOLO_ON' + # row.prop(propsx, pn, icon=icon, icon_only=True) + # print(dir(props)) + # new best method - enum with an items callback. ('animates' the stars as item icons) + row.prop(props, 'rating_quality_ui', expand=True, icon_only=True, emboss = False) + # original (operator) method: + # row = layout.row(align=True) + # for a in range(0, 10): + # if eval('props.' + prop_name) < a + 1: + # icon = 'SOLO_OFF' + # else: + # icon = 'SOLO_ON' + # + # op = row.operator('object.blenderkit_rating', icon=icon, emboss=False, text='') + # op.property_name = prop_name + # op.rating = a + 1 def register_ratings(): pass; - bpy.utils.register_class(StarRatingOperator) + # bpy.utils.register_class(StarRatingOperator) bpy.utils.register_class(UploadRatingOperator) def unregister_ratings(): pass; - bpy.utils.unregister_class(StarRatingOperator) + # bpy.utils.unregister_class(StarRatingOperator) bpy.utils.unregister_class(UploadRatingOperator) diff --git a/blenderkit/rerequests.py b/blenderkit/rerequests.py index 587093e0..3d9a4d75 100644 --- a/blenderkit/rerequests.py +++ b/blenderkit/rerequests.py @@ -40,7 +40,7 @@ def rerequest(method, url, **kwargs): # first normal attempt response = requests.request(method, url, **kwargs) - utils.p(url) + utils.p(url, kwargs) utils.p(response.status_code) if response.status_code == 401: diff --git a/blenderkit/search.py b/blenderkit/search.py index bae25466..09dfeb65 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -134,7 +134,7 @@ def scene_load(context): def fetch_server_data(): - ''' download categories and addon version''' + ''' download categories , profile, and refresh token if needed.''' if not bpy.app.background: user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index 1083b6a6..b8d8ce73 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -33,58 +33,32 @@ from bpy.types import ( ) import bpy +import os +import random -def label_multiline(layout, text='', icon='NONE', width=-1): - ''' draw a ui label, but try to split it in multiple lines.''' - if text.strip() == '': - return - lines = text.split('\n') - if width > 0: - threshold = int(width / 5.5) - else: - threshold = 35 - maxlines = 8 - li = 0 - for l in lines: - while len(l) > threshold: - i = l.rfind(' ', 0, threshold) - if i < 1: - i = threshold - l1 = l[:i] - layout.label(text=l1, icon=icon) - icon = 'NONE' - l = l[i:].lstrip() - li += 1 - if li > maxlines: - break; - if li > maxlines: - break; - layout.label(text=l, icon=icon) - icon = 'NONE' - # this was moved to separate interface: -def draw_ratings(layout, context): +def draw_ratings(layout, context, asset): # layout.operator("wm.url_open", text="Read rating instructions", icon='QUESTION').url = 'https://support.google.com/?hl=en' - asset = utils.get_active_asset() # the following shouldn't happen at all in an optimal case, # this function should run only when asset was already checked to be existing if asset == None: return; col = layout.column() - if not utils.user_logged_in(): - label_multiline(layout, text='Please login or sign up ' - 'to rate assets.') - col.enabled = False bkit_ratings = asset.bkit_ratings - ratings.draw_rating(col, bkit_ratings, 'rating_quality', 'Quality') - col.separator() - col.prop(bkit_ratings, 'rating_work_hours') - w = context.region.width + # layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0) + + row = col.row() + row.prop(bkit_ratings , 'rating_quality_ui', expand=True, icon_only=True, emboss=False) + #ratings.draw_rating(col, bkit_ratings, 'rating_quality', 'Quality') + if bkit_ratings.rating_quality>0: + col.separator() + col.prop(bkit_ratings, 'rating_work_hours') + # w = context.region.width # layout.label(text='problems') # layout.prop(bkit_ratings, 'rating_problems', text='') @@ -97,13 +71,12 @@ def draw_ratings(layout, context): # re-enable layout if included in longer panel -def draw_not_logged_in(source): - title = "User not logged in" +def draw_not_logged_in(source, message = 'Please Login/Signup to use this feature' ): + title = "You aren't logged in" def draw_message(source, context): layout = source.layout - label_multiline(layout, text='Please login or sign up ' - 'to upload files.') + utils.label_multiline(layout, text=message) draw_login_buttons(layout) bpy.context.window_manager.popup_menu(draw_message, title=title, icon='INFO') @@ -121,7 +94,7 @@ def draw_upload_common(layout, props, asset_type, context): row = layout.row(align=True) if props.upload_state != '': - label_multiline(layout, text=props.upload_state, width=context.region.width) + utils.label_multiline(layout, text=props.upload_state, width=context.region.width) if props.uploading: op = layout.operator('object.kill_bg_process', text="", icon='CANCEL') op.process_source = asset_type @@ -210,7 +183,7 @@ def draw_panel_model_upload(self, context): op.process_source = 'MODEL' op.process_type = 'THUMBNAILER' elif props.thumbnail_generating_state != '': - label_multiline(layout, text=props.thumbnail_generating_state) + utils.label_multiline(layout, text=props.thumbnail_generating_state) layout.prop(props, 'description') layout.prop(props, 'tags') @@ -274,7 +247,7 @@ def draw_panel_scene_upload(self, context): # op.process_source = 'MODEL' # op.process_type = 'THUMBNAILER' # elif props.thumbnail_generating_state != '': - # label_multiline(layout, text = props.thumbnail_generating_state) + # utils.label_multiline(layout, text = props.thumbnail_generating_state) layout.prop(props, 'description') layout.prop(props, 'tags') @@ -319,7 +292,7 @@ def draw_panel_model_search(self, context): icon = 'NONE' if props.report == 'You need Full plan to get this item.': icon = 'ERROR' - label_multiline(layout, text=props.report, icon=icon) + utils.label_multiline(layout, text=props.report, icon=icon) if props.report == 'You need Full plan to get this item.': layout.operator("wm.url_open", text="Get Full plan", icon='URL').url = paths.BLENDERKIT_PLANS @@ -346,7 +319,7 @@ def draw_panel_scene_search(self, context): row.prop(props, "search_keywords", text="", icon='VIEWZOOM') draw_assetbar_show_hide(row, props) layout.prop(props, "own_only") - label_multiline(layout, text=props.report) + utils.label_multiline(layout, text=props.report) # layout.prop(props, "search_style") # if props.search_style == 'OTHER': @@ -376,7 +349,7 @@ class VIEW3D_PT_blenderkit_model_properties(Panel): o = utils.get_active_model() # o = bpy.context.active_object if o.get('asset_data') is None: - label_multiline(layout, text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') + utils.label_multiline(layout, text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') layout.prop(o, 'name') if o.get('asset_data') is not None: @@ -400,6 +373,51 @@ class VIEW3D_PT_blenderkit_model_properties(Panel): # fun override project, not finished # layout.operator('object.blenderkit_color_corrector') +def draw_rating_asset(self,context,asset): + layout = self.layout + col = layout.box() + # split = layout.split(factor=0.5) + # col1 = split.column() + # col2 = split.column() + directory = paths.get_temp_dir('%s_search' % asset['asset_data']['assetType']) + tpath = os.path.join(directory, asset['asset_data']['thumbnail_small']) + for image in bpy.data.images: + if image.filepath == tpath: + # split = row.split(factor=1.0, align=False) + col.template_icon(icon_value=image.preview.icon_id, scale=6.0) + break; + # layout.label(text = '', icon_value=image.preview.icon_id, scale = 10) + col.label(text=asset.name) + draw_ratings(col, context, asset=asset) + + + + +class VIEW3D_PT_blenderkit_ratings(Panel): + bl_category = "BlenderKit" + bl_idname = "VIEW3D_PT_blenderkit_ratings" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Please rate" + bl_context = "objectmode" + + @classmethod + def poll(cls, context): + # + p = bpy.context.view_layer.objects.active is not None + return p + + def draw(self, context): + #TODO make a list of assets inside asset appending code, to happen only when assets are added to the scene. + # draw asset properties here + layout = self.layout + assets = ratings.get_assets_for_rating() + if len(assets)>0: + layout.label(text = 'Help BlenderKit community') + layout.label(text = 'by rating these assets:') + + for a in assets: + draw_rating_asset(self, context, asset = a) def draw_login_progress(layout): layout.label(text='Login through browser') @@ -492,7 +510,7 @@ def draw_panel_model_rating(self, context): # o = bpy.context.active_object o = utils.get_active_model() # print('ratings active',o) - draw_ratings(self.layout, context) # , props) + draw_ratings(self.layout, context, asset = o) # , props) # op.asset_type = 'MODEL' @@ -536,7 +554,7 @@ def draw_panel_material_upload(self, context): op.process_source = 'MATERIAL' op.process_type = 'THUMBNAILER' elif props.thumbnail_generating_state != '': - label_multiline(layout, text=props.thumbnail_generating_state) + utils.label_multiline(layout, text=props.thumbnail_generating_state) if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'): layout.operator("object.blenderkit_material_thumbnail", text='Render thumbnail with Cycles', icon='EXPORT') @@ -557,9 +575,9 @@ def draw_panel_material_search(self, context): row.prop(props, "search_keywords", text="", icon='VIEWZOOM') draw_assetbar_show_hide(row, props) layout.prop(props, "own_only") - label_multiline(layout, text=props.report) + utils.label_multiline(layout, text=props.report) - # layout.prop(props, 'search_style') + # layout.prop(props, 'search_style')F # if props.search_style == 'OTHER': # layout.prop(props, 'search_style_other') # layout.prop(props, 'search_engine') @@ -570,7 +588,8 @@ def draw_panel_material_search(self, context): def draw_panel_material_ratings(self, context): - draw_ratings(self.layout, context) # , props) + asset = bpy.context.active_object.active_material + draw_ratings(self.layout, context, asset) # , props) # op.asset_type = 'MATERIAL' @@ -598,23 +617,28 @@ def draw_panel_brush_search(self, context): draw_assetbar_show_hide(row, props) layout.prop(props, "own_only") - label_multiline(layout, text=props.report) + utils.label_multiline(layout, text=props.report) # draw_panel_categories(self, context) def draw_panel_brush_ratings(self, context): # props = utils.get_brush_props(context) - draw_ratings(self.layout, context) # , props) + brush = utils.get_active_brush() + draw_ratings(self.layout, context, asset = brush) # , props) # # op.asset_type = 'BRUSH' -def draw_login_buttons(layout): +def draw_login_buttons(layout, invoke = False): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences if user_preferences.login_attempt: draw_login_progress(layout) else: + if invoke: + layout.operator_context = 'INVOKE_DEFAULT' + else: + layout.operator_context = 'EXEC_DEFAULT' if user_preferences.api_key == '': layout.operator("wm.blenderkit_login", text="Login", icon='URL').signup = False @@ -846,7 +870,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): layout.separator() # if bpy.data.filepath == '': # layout.alert = True - # label_multiline(layout, text="It's better to save your file first.", width=w) + # utils.label_multiline(layout, text="It's better to save your file first.", width=w) # layout.alert = False # layout.separator() @@ -868,7 +892,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): # noinspection PyCallByClass draw_panel_brush_search(self, context) else: - label_multiline(layout, text='switch to paint or sculpt mode.', width=context.region.width) + utils.label_multiline(layout, text='switch to paint or sculpt mode.', width=context.region.width) return @@ -886,11 +910,11 @@ class VIEW3D_PT_blenderkit_unified(Panel): if e not in ('CYCLES', 'BLENDER_EEVEE'): rtext = 'Only Cycles and EEVEE render engines are currently supported. ' \ 'Please use Cycles for all assets you upload to BlenderKit.' - label_multiline(layout, rtext, icon='ERROR', width=w) + utils.label_multiline(layout, rtext, icon='ERROR', width=w) return; if ui_props.asset_type == 'MODEL': - # label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') + #utils.label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None: draw_panel_model_upload(self, context) else: @@ -899,12 +923,12 @@ class VIEW3D_PT_blenderkit_unified(Panel): draw_panel_scene_upload(self, context) elif ui_props.asset_type == 'MATERIAL': - # label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') + #utils.label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: draw_panel_material_upload(self, context) else: - label_multiline(layout, text='select object with material to upload materials', width=w) + utils.label_multiline(layout, text='select object with material to upload materials', width=w) elif ui_props.asset_type == 'BRUSH': if context.sculpt_object or context.image_paint_object: @@ -1029,6 +1053,16 @@ class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): asset_data = sr[ui_props.active_index] draw_asset_context_menu(self, context, asset_data) +class OBJECT_MT_blenderkit_login_menu(bpy.types.Menu): + bl_label = "BlenderKit login/signup:" + bl_idname = "OBJECT_MT_blenderkit_login_menu" + + def draw(self, context): + layout = self.layout + + # utils.label_multiline(layout, text=message) + draw_login_buttons(layout) + class SetCategoryOperator(bpy.types.Operator): """Visit subcategory""" @@ -1088,9 +1122,42 @@ class UrlPopupDialog(bpy.types.Operator): def draw(self, context): layout = self.layout - label_multiline(layout, text=self.message) + utils.label_multiline(layout, text=self.message) + + layout.active_default = True + op = layout.operator("wm.url_open", text=self.link_text, icon='QUESTION') + op.url = self.url + + def execute(self, context): + # start_thumbnailer(self, context) + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + + return wm.invoke_props_dialog(self) + +class LoginPopupDialog(bpy.types.Operator): + """Generate Cycles thumbnail for model assets""" + bl_idname = "wm.blenderkit_url_dialog" + bl_label = "BlenderKit login" + bl_options = {'REGISTER', 'INTERNAL'} + + message: bpy.props.StringProperty( + name="Message", + description="", + default="Your were logged out from BlenderKit. Please login again. ") + + # @classmethod + # def poll(cls, context): + # return bpy.context.view_layer.objects.active is not None + + def draw(self, context): + layout = self.layout + utils.label_multiline(layout, text=self.message) layout.active_default = True + op = layout.operator op = layout.operator("wm.url_open", text=self.link_text, icon='QUESTION') op.url = self.url @@ -1227,8 +1294,10 @@ classess = ( VIEW3D_PT_blenderkit_categories, VIEW3D_PT_blenderkit_import_settings, VIEW3D_PT_blenderkit_model_properties, + # VIEW3D_PT_blenderkit_ratings, VIEW3D_PT_blenderkit_downloads, OBJECT_MT_blenderkit_asset_menu, + OBJECT_MT_blenderkit_login_menu, UrlPopupDialog ) diff --git a/blenderkit/upload.py b/blenderkit/upload.py index 3afe9815..18e43b8a 100644 --- a/blenderkit/upload.py +++ b/blenderkit/upload.py @@ -777,7 +777,7 @@ class UploadOperator(Operator): props = utils.get_upload_props() if not utils.user_logged_in(): - ui_panels.draw_not_logged_in(self) + ui_panels.draw_not_logged_in(self, message = 'To upload assets you need to login/signup.') return {'CANCELLED'} if props.is_private == 'PUBLIC': diff --git a/blenderkit/utils.py b/blenderkit/utils.py index 503a93f2..2e59887c 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -263,6 +263,15 @@ def save_prefs(self, context): except Exception as e: print(e) +def get_hidden_texture(tpath, bdata_name, force_reload = False): + i = get_hidden_image(tpath, bdata_name, force_reload = force_reload) + bdata_name = f".{bdata_name}" + t = bpy.data.textures.get(bdata_name) + if t is None: + t = bpy.data.textures.new('.test', 'IMAGE') + if t.image!= i: + t.image = i + return t def get_hidden_image(tpath, bdata_name, force_reload=False): hidden_name = '.%s' % bdata_name @@ -603,3 +612,32 @@ def guard_from_crash(): if bpy.context.preferences.addons['blenderkit'].preferences is None: return False; return True + + +def label_multiline(layout, text='', icon='NONE', width=-1): + ''' draw a ui label, but try to split it in multiple lines.''' + if text.strip() == '': + return + lines = text.split('\n') + if width > 0: + threshold = int(width / 5.5) + else: + threshold = 35 + maxlines = 8 + li = 0 + for l in lines: + while len(l) > threshold: + i = l.rfind(' ', 0, threshold) + if i < 1: + i = threshold + l1 = l[:i] + layout.label(text=l1, icon=icon) + icon = 'NONE' + l = l[i:].lstrip() + li += 1 + if li > maxlines: + break; + if li > maxlines: + break; + layout.label(text=l, icon=icon) + icon = 'NONE' \ No newline at end of file -- cgit v1.2.3 From cb3f8ec4b1c46e3130c9fb8f20cc8e892442c941 Mon Sep 17 00:00:00 2001 From: Mikhail Rachinskiy Date: Wed, 15 Jul 2020 05:32:14 +0400 Subject: PLY: avoid list to dict conversion Use dictionary comprehension instead of converting list comprehension to dictionary. Gives around 2.5% speedup, and will probably scale in favor of dict comprehension. --- io_mesh_ply/import_ply.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/io_mesh_ply/import_ply.py b/io_mesh_ply/import_ply.py index 2bf91442..915368d7 100644 --- a/io_mesh_ply/import_ply.py +++ b/io_mesh_ply/import_ply.py @@ -110,7 +110,12 @@ class ObjectSpec: self.specs = [] def load(self, format, stream): - return dict([(i.name, [i.load(format, stream) for j in range(i.count)]) for i in self.specs]) + return { + i.name: [ + i.load(format, stream) for j in range(i.count) + ] + for i in self.specs + } # Longhand for above LC """ -- cgit v1.2.3 From 44bd5400357ff8dc2baa30dc2d1836196f900c1a Mon Sep 17 00:00:00 2001 From: Mikhail Rachinskiy Date: Wed, 15 Jul 2020 05:53:53 +0400 Subject: PLY Cleanup: redundant comments --- io_mesh_ply/__init__.py | 6 +++--- io_mesh_ply/import_ply.py | 12 ------------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/io_mesh_ply/__init__.py b/io_mesh_ply/__init__.py index 5e12bfce..79b8dc38 100644 --- a/io_mesh_ply/__init__.py +++ b/io_mesh_ply/__init__.py @@ -189,7 +189,7 @@ class PLY_PT_export_include(bpy.types.Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - layout.use_property_decorate = False # No animation. + layout.use_property_decorate = False sfile = context.space_data operator = sfile.active_operator @@ -213,7 +213,7 @@ class PLY_PT_export_transform(bpy.types.Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - layout.use_property_decorate = False # No animation. + layout.use_property_decorate = False sfile = context.space_data operator = sfile.active_operator @@ -239,7 +239,7 @@ class PLY_PT_export_geometry(bpy.types.Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - layout.use_property_decorate = False # No animation. + layout.use_property_decorate = False sfile = context.space_data operator = sfile.active_operator diff --git a/io_mesh_ply/import_ply.py b/io_mesh_ply/import_ply.py index 915368d7..c7981cc3 100644 --- a/io_mesh_ply/import_ply.py +++ b/io_mesh_ply/import_ply.py @@ -117,18 +117,6 @@ class ObjectSpec: for i in self.specs } - # Longhand for above LC - """ - answer = {} - for i in self.specs: - answer[i.name] = [] - for j in range(i.count): - if not j % 100 and meshtools.show_progress: - Blender.Window.DrawProgressBar(float(j) / i.count, 'Loading ' + i.name) - answer[i.name].append(i.load(format, stream)) - return answer - """ - def read(filepath): import re -- cgit v1.2.3 From e9ada0c8e31cf0f4329f8ecc90c4fb970cbf948b Mon Sep 17 00:00:00 2001 From: Mikhail Rachinskiy Date: Wed, 15 Jul 2020 06:04:42 +0400 Subject: PLY Cleanup: formatting --- io_mesh_ply/__init__.py | 33 +++++++++++++++------------------ io_mesh_ply/import_ply.py | 2 +- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/io_mesh_ply/__init__.py b/io_mesh_ply/__init__.py index 79b8dc38..835ae60e 100644 --- a/io_mesh_ply/__init__.py +++ b/io_mesh_ply/__init__.py @@ -52,7 +52,7 @@ from bpy_extras.io_utils import ( ImportHelper, ExportHelper, axis_conversion, - orientation_helper + orientation_helper, ) @@ -64,11 +64,9 @@ class ImportPLY(bpy.types.Operator, ImportHelper): files: CollectionProperty( name="File Path", - description=( - "File path used for importing " - "the PLY file" - ), - type=bpy.types.OperatorFileListElement) + description="File path used for importing the PLY file", + type=bpy.types.OperatorFileListElement, + ) # Hide opertator properties, rest of this is managed in C. See WM_operator_properties_filesel(). hide_props_region: BoolProperty( @@ -84,14 +82,16 @@ class ImportPLY(bpy.types.Operator, ImportHelper): def execute(self, context): import os + from . import import_ply + + paths = [ + os.path.join(self.directory, name.name) + for name in self.files + ] - paths = [os.path.join(self.directory, name.name) - for name in self.files] if not paths: paths.append(self.filepath) - from . import import_ply - for path in paths: import_ply.load(self, context, path) @@ -120,10 +120,8 @@ class ExportPLY(bpy.types.Operator, ExportHelper): use_normals: BoolProperty( name="Normals", description=( - "Export Normals for smooth and " - "hard shaded faces " - "(hard shaded faces will be exported " - "as individual faces)" + "Export Normals for smooth and hard shaded faces " + "(hard shaded faces will be exported as individual faces)" ), default=True, ) @@ -137,17 +135,16 @@ class ExportPLY(bpy.types.Operator, ExportHelper): description="Export the active vertex color layer", default=True, ) - global_scale: FloatProperty( name="Scale", - min=0.01, max=1000.0, + min=0.01, + max=1000.0, default=1.0, ) def execute(self, context): - from . import export_ply - from mathutils import Matrix + from . import export_ply keywords = self.as_keywords( ignore=( diff --git a/io_mesh_ply/import_ply.py b/io_mesh_ply/import_ply.py index c7981cc3..6df2ec81 100644 --- a/io_mesh_ply/import_ply.py +++ b/io_mesh_ply/import_ply.py @@ -275,7 +275,7 @@ def load_ply_mesh(filepath, ply_name): vertices[index][colindices[0]] * colmultiply[0], vertices[index][colindices[1]] * colmultiply[1], vertices[index][colindices[2]] * colmultiply[2], - 1.0 + 1.0, ) for index in indices ]) -- cgit v1.2.3 From 19a666fb7f097ee8963223775d15da297f20e7fb Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Wed, 15 Jul 2020 11:27:34 +0300 Subject: Rigify: make sure not to copy certain properties in copy_custom_properties. --- rigify/utils/mechanism.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rigify/utils/mechanism.py b/rigify/utils/mechanism.py index 3e7b2990..232fb7af 100644 --- a/rigify/utils/mechanism.py +++ b/rigify/utils/mechanism.py @@ -351,9 +351,10 @@ def reactivate_custom_properties(obj): def copy_custom_properties(src, dest, *, prefix='', dest_prefix='', link_driver=False): """Copy custom properties with filtering by prefix. Optionally link using drivers.""" res = [] + exclude = {'_RNA_UI', 'rigify_parameters', 'rigify_type'} for key, value in src.items(): - if key.startswith(prefix): + if key.startswith(prefix) and key not in exclude: new_key = dest_prefix + key[len(prefix):] dest[new_key] = value -- cgit v1.2.3 From 38dfced7bba68b38b17322c0ea2acf18f27c1804 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Wed, 15 Jul 2020 11:37:16 +0300 Subject: Fix T78864: the apply_as parameter of modifier_apply was removed. Explicitly specifying DATA was redundant anyway, as that was the default. --- add_mesh_extra_objects/add_mesh_round_brilliant.py | 2 +- archipack/archipack_reference_point.py | 3 +-- mesh_auto_mirror.py | 3 +-- mesh_bsurfaces.py | 4 ++-- mesh_tissue/dual_mesh.py | 4 +--- object_carver/carver_operator.py | 6 +++--- object_carver/carver_utils.py | 8 ++++---- object_skinify.py | 4 ++-- space_view3d_modifier_tools.py | 2 +- 9 files changed, 16 insertions(+), 20 deletions(-) diff --git a/add_mesh_extra_objects/add_mesh_round_brilliant.py b/add_mesh_extra_objects/add_mesh_round_brilliant.py index 49232151..bdd9b68c 100644 --- a/add_mesh_extra_objects/add_mesh_round_brilliant.py +++ b/add_mesh_extra_objects/add_mesh_round_brilliant.py @@ -298,7 +298,7 @@ def addBrilliant(context, self, s, table_w, crown_h, girdle_t, pavi_d, bezel_f, bpy.context.tool_settings.mesh_select_mode = sel_mode bpy.ops.object.mode_set(mode='OBJECT', toggle=False) - bpy.ops.object.modifier_apply(apply_as='DATA', modifier="EdgeSplit") + bpy.ops.object.modifier_apply(modifier="EdgeSplit") return dobj diff --git a/archipack/archipack_reference_point.py b/archipack/archipack_reference_point.py index 99acf9d8..5548511c 100644 --- a/archipack/archipack_reference_point.py +++ b/archipack/archipack_reference_point.py @@ -298,8 +298,7 @@ class ARCHIPACK_OT_apply_holes(Operator): for mod in o.modifiers[:]: ctx['modifier'] = mod try: - bpy.ops.object.modifier_apply(ctx, apply_as='DATA', - modifier=mod.name) + bpy.ops.object.modifier_apply(ctx, modifier=mod.name) except: pass diff --git a/mesh_auto_mirror.py b/mesh_auto_mirror.py index 1d89d4e7..47611668 100644 --- a/mesh_auto_mirror.py +++ b/mesh_auto_mirror.py @@ -194,8 +194,7 @@ class AutoMirror(bpy.types.Operator): bpy.context.object.modifiers[-1].show_on_cage = automirror.show_on_cage if automirror.apply_mirror: bpy.ops.object.mode_set(mode = 'OBJECT') - bpy.ops.object.modifier_apply(apply_as = 'DATA', - modifier = bpy.context.object.modifiers[-1].name) + bpy.ops.object.modifier_apply(modifier = bpy.context.object.modifiers[-1].name) if automirror.toggle_edit: bpy.ops.object.mode_set(mode = 'EDIT') else: diff --git a/mesh_bsurfaces.py b/mesh_bsurfaces.py index c0c7a4f9..9e7f7a59 100644 --- a/mesh_bsurfaces.py +++ b/mesh_bsurfaces.py @@ -1660,7 +1660,7 @@ class MESH_OT_SURFSK_add_surface(Operator): shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX" shrinkwrap_modifier.target = self.main_object - bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier=shrinkwrap_modifier.name) + bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name) # Make list with verts of original mesh as index and coords as value main_object_verts_coords = [] @@ -4010,7 +4010,7 @@ class CURVE_OT_SURFSK_reorder_splines(Operator): bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP') curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX" curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh - bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', apply_as='DATA', modifier='Shrinkwrap') + bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier='Shrinkwrap') # Get the distance of each vert from its original position to its position with Shrinkwrap nearest_points_coords = {} diff --git a/mesh_tissue/dual_mesh.py b/mesh_tissue/dual_mesh.py index db24f896..404d5ef5 100644 --- a/mesh_tissue/dual_mesh.py +++ b/mesh_tissue/dual_mesh.py @@ -240,9 +240,7 @@ class dual_mesh(Operator): if ob.modifiers[0].name == "dual_mesh_subsurf": break - bpy.ops.object.modifier_apply( - apply_as='DATA', modifier='dual_mesh_subsurf' - ) + bpy.ops.object.modifier_apply(modifier='dual_mesh_subsurf') bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') diff --git a/object_carver/carver_operator.py b/object_carver/carver_operator.py index 95fa4af0..880f6491 100644 --- a/object_carver/carver_operator.py +++ b/object_carver/carver_operator.py @@ -1196,7 +1196,7 @@ class CARVER_OT_operator(bpy.types.Operator): for mb in ActiveObj.modifiers: if (mb.type == 'BOOLEAN') and (mb.name == BMname): try: - bpy.ops.object.modifier_apply(apply_as='DATA', modifier=BMname) + bpy.ops.object.modifier_apply(modifier=BMname) except: bpy.ops.object.modifier_remove(modifier=BMname) exc_type, exc_value, exc_traceback = sys.exc_info() @@ -1208,7 +1208,7 @@ class CARVER_OT_operator(bpy.types.Operator): for mb in self.CurrentObj.modifiers: if (mb.type == 'SOLIDIFY') and (mb.name == "CT_SOLIDIFY"): try: - bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_SOLIDIFY") + bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) @@ -1243,7 +1243,7 @@ class CARVER_OT_operator(bpy.types.Operator): for mb in ActiveObj.modifiers: if (mb.type == 'BOOLEAN') and (mb.name == BMname): try: - bpy.ops.object.modifier_apply(apply_as='DATA', modifier=BMname) + bpy.ops.object.modifier_apply(modifier=BMname) except: bpy.ops.object.modifier_remove(modifier=BMname) exc_type, exc_value, exc_traceback = sys.exc_info() diff --git a/object_carver/carver_utils.py b/object_carver/carver_utils.py index 495aa1ce..1bd7455f 100644 --- a/object_carver/carver_utils.py +++ b/object_carver/carver_utils.py @@ -695,7 +695,7 @@ def boolean_operation(bool_type="DIFFERENCE"): ActiveObj = bpy.context.active_object sel_index = 0 if bpy.context.selected_objects[0] != bpy.context.active_object else 1 - # bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_SOLIDIFY") + # bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY") bool_name = "CT_" + bpy.context.selected_objects[sel_index].name BoolMod = ActiveObj.modifiers.new(bool_name, "BOOLEAN") BoolMod.object = bpy.context.selected_objects[sel_index] @@ -736,14 +736,14 @@ def Rebool(context, self): if self.ObjectBrush or self.ProfileBrush: rebool_obj.show_in_front = False try: - bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_SOLIDIFY") + bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) if self.dont_apply_boolean is False: try: - bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_INTERSECT") + bpy.ops.object.modifier_apply(modifier="CT_INTERSECT") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) @@ -758,7 +758,7 @@ def Rebool(context, self): target_obj.select_set(True) if self.dont_apply_boolean is False: try: - bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_DIFFERENCE") + bpy.ops.object.modifier_apply(modifier="CT_DIFFERENCE") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) diff --git a/object_skinify.py b/object_skinify.py index 177f8de9..f102d8cf 100644 --- a/object_skinify.py +++ b/object_skinify.py @@ -551,8 +551,8 @@ def generate_mesh(shape_object, size, thickness=0.8, finger_thickness=0.25, sub_ # object mode apply all modifiers if apply_mod: - bpy.ops.object.modifier_apply(override, apply_as='DATA', modifier="Skin") - bpy.ops.object.modifier_apply(override, apply_as='DATA', modifier="Subsurf") + bpy.ops.object.modifier_apply(override, modifier="Skin") + bpy.ops.object.modifier_apply(override, modifier="Subsurf") return {'FINISHED'} diff --git a/space_view3d_modifier_tools.py b/space_view3d_modifier_tools.py index aaef9b6a..1fb64635 100644 --- a/space_view3d_modifier_tools.py +++ b/space_view3d_modifier_tools.py @@ -58,7 +58,7 @@ class ApplyAllModifiers(Operator): is_mod = True try: bpy.ops.object.modifier_apply( - contx, apply_as='DATA', + contx, modifier=contx['modifier'].name ) except: -- cgit v1.2.3 From c279fa91c16cd53e9984416a6ac554850589cfca Mon Sep 17 00:00:00 2001 From: Eugenio Pignataro Date: Thu, 16 Jul 2020 11:36:29 -0300 Subject: Add Copy Paste uv islands --- oscurart_tools/mesh/overlap_uvs.py | 96 +++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/oscurart_tools/mesh/overlap_uvs.py b/oscurart_tools/mesh/overlap_uvs.py index d0d13752..c5bbab05 100644 --- a/oscurart_tools/mesh/overlap_uvs.py +++ b/oscurart_tools/mesh/overlap_uvs.py @@ -27,66 +27,57 @@ from bpy.props import ( FloatProperty, EnumProperty, ) -import os + + import bmesh -C = bpy.context -D = bpy.data - - - # -------------------------- OVERLAP UV ISLANDS def defCopyUvsIsland(self, context): - bpy.ops.object.mode_set(mode="OBJECT") - global obLoop - global islandFaces - obLoop = [] - islandFaces = [] - for poly in bpy.context.object.data.polygons: - if poly.select: - islandFaces.append(poly.index) - for li in poly.loop_indices: - obLoop.append(li) - - bpy.ops.object.mode_set(mode="EDIT") + global islandSet + islandSet = {} + islandSet["Loop"] = [] + + bpy.context.scene.tool_settings.use_uv_select_sync = True + bpy.ops.uv.select_linked() + bm = bmesh.from_edit_mesh(bpy.context.object.data) + uv_lay = bm.loops.layers.uv.active + faceSel = 0 + for face in bm.faces: + if face.select: + faceSel +=1 + for loop in face.loops: + islandSet["Loop"].append(loop[uv_lay].uv.copy()) + islandSet["Size"] = faceSel def defPasteUvsIsland(self, uvOffset, rotateUv,context): - bpy.ops.object.mode_set(mode="OBJECT") - selPolys = [poly.index for poly in bpy.context.object.data.polygons if poly.select] - - for island in selPolys: - bpy.ops.object.mode_set(mode="EDIT") + bm = bmesh.from_edit_mesh(bpy.context.object.data) + bpy.context.scene.tool_settings.use_uv_select_sync = True + pickedFaces = [face for face in bm.faces if face.select] + for face in pickedFaces: bpy.ops.mesh.select_all(action="DESELECT") - bpy.ops.object.mode_set(mode="OBJECT") - bpy.context.object.data.polygons[island].select = True - bpy.ops.object.mode_set(mode="EDIT") - bpy.ops.mesh.select_linked() - bpy.ops.object.mode_set(mode="OBJECT") - TobLoop = [] - TislandFaces = [] - for poly in bpy.context.object.data.polygons: - if poly.select: - TislandFaces.append(poly.index) - for li in poly.loop_indices: - TobLoop.append(li) - - for source,target in zip(range(min(obLoop),max(obLoop)+1),range(min(TobLoop),max(TobLoop)+1)): - bpy.context.object.data.uv_layers.active.data[target].uv = bpy.context.object.data.uv_layers.active.data[source].uv + Vector((uvOffset,0)) - - bpy.ops.object.mode_set(mode="EDIT") - - if rotateUv: - bpy.ops.object.mode_set(mode="OBJECT") - for poly in selPolys: - bpy.context.object.data.polygons[poly].select = True - bpy.ops.object.mode_set(mode="EDIT") - bm = bmesh.from_edit_mesh(bpy.context.object.data) - bmesh.ops.reverse_uvs(bm, faces=[f for f in bm.faces if f.select]) - bmesh.ops.rotate_uvs(bm, faces=[f for f in bm.faces if f.select]) - #bmesh.update_edit_mesh(bpy.context.object.data, tessface=False, destructive=False) - - + face.select=True + bmesh.update_edit_mesh(bpy.context.object.data) + bpy.ops.uv.select_linked() + uv_lay = bm.loops.layers.uv.active + faceSel = 0 + for face in bm.faces: + if face.select: + faceSel +=1 + i = 0 + if faceSel == islandSet["Size"]: + for face in bm.faces: + if face.select: + for loop in face.loops: + loop[uv_lay].uv = islandSet["Loop"][i] if uvOffset == False else islandSet["Loop"][i]+Vector((1,0)) + i += 1 + else: + print("the island have a different size of geometry") + + if rotateUv: + bpy.ops.object.mode_set(mode="EDIT") + bmesh.ops.reverse_uvs(bm, faces=[f for f in bm.faces if f.select]) + bmesh.ops.rotate_uvs(bm, faces=[f for f in bm.faces if f.select]) class CopyUvIsland(Operator): """Copy Uv Island""" @@ -119,6 +110,7 @@ class PasteUvIsland(Operator): name="Rotate Uv Corner", default=False ) + @classmethod def poll(cls, context): return (context.active_object is not None and -- cgit v1.2.3 From 565ae1b26e9924ed1973ca270e49200f42d6fadc Mon Sep 17 00:00:00 2001 From: CansecoGPC Date: Thu, 16 Jul 2020 17:43:51 +0200 Subject: Sapling Tree Gen: Set quaking_aspen.py and willow.py presets bevel to True --- add_curve_sapling/presets/quaking_aspen.py | 2 +- add_curve_sapling/presets/willow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/add_curve_sapling/presets/quaking_aspen.py b/add_curve_sapling/presets/quaking_aspen.py index 6cab7386..70e0a80b 100644 --- a/add_curve_sapling/presets/quaking_aspen.py +++ b/add_curve_sapling/presets/quaking_aspen.py @@ -1 +1 @@ -{'leafScale': 0.17000000178813934, 'autoTaper': True, 'customShape': (0.5, 1.0, 0.30000001192092896, 0.5), 'leafShape': 'hex', 'curve': (0.0, -40.0, -40.0, 0.0), 'ratio': 0.014999999664723873, 'splitBias': 0.0, 'pruneWidth': 0.4000000059604645, 'downAngleV': (0.0, 80.0, 10.0, 10.0), 'rotate': (99.5, 137.5, 137.5, 137.5), 'pruneRatio': 1.0, 'leafDownAngle': 45.0, 'makeMesh': False, 'radiusTweak': (1.0, 1.0, 1.0, 1.0), 'rMode': 'rotate', 'splitAngleV': (0.0, 0.0, 0.0, 0.0), 'branchDist': 1.0, 'bevel': False, 'minRadius': 0.001500000013038516, 'prune': False, 'leafRotateV': 0.0, 'splitAngle': (0.0, 0.0, 0.0, 0.0), 'armAnim': False, 'boneStep': (1, 1, 1, 1), 'pruneBase': 0.30000001192092896, 'taperCrown': 0.0, 'baseSplits': 0, 'baseSize_s': 0.25, 'handleType': '0', 'baseSize': 0.4000000059604645, 'af1': 1.0, 'levels': 2, 'leafScaleV': 0.0, 'resU': 4, 'seed': 0, 'downAngle': (90.0, 110.0, 45.0, 45.0), 'leafangle': 0.0, 'scaleV0': 0.10000000149011612, 'prunePowerHigh': 0.5, 'splitByLen': True, 'wind': 1.0, 'shape': '7', 'prunePowerLow': 0.0010000000474974513, 'scale': 13.0, 'leafAnim': False, 'curveBack': (0.0, 0.0, 0.0, 0.0), 'leafScaleX': 1.0, 'horzLeaves': True, 'splitHeight': 0.20000000298023224, 'leafScaleT': 0.0, 'scaleV': 3.0, 'leafDist': '6', 'nrings': 0, 'curveRes': (8, 5, 3, 1), 'shapeS': '4', 'bevelRes': 0, 'useOldDownAngle': False, 'useParentAngle': True, 'armLevels': 2, 'scale0': 1.0, 'taper': (1.0, 1.0, 1.0, 1.0), 'pruneWidthPeak': 0.6000000238418579, 'previewArm': False, 'leaves': 25, 'ratioPower': 1.100000023841858, 'gustF': 0.07500000298023224, 'curveV': (20.0, 50.0, 75.0, 0.0), 'showLeaves': False, 'frameRate': 1.0, 'length': (1.0, 0.30000001192092896, 0.6000000238418579, 0.44999998807907104), 'branches': (0, 50, 30, 10), 'useArm': False, 'loopFrames': 0, 'gust': 1.0, 'af3': 4.0, 'closeTip': False, 'leafRotate': 137.5, 'attractUp': (0.0, 0.0, 0.5, 0.5), 'leafDownAngleV': 10.0, 'rootFlare': 1.0, 'af2': 1.0, 'lengthV': (0.0, 0.0, 0.0, 0.0), 'rotateV': (15.0, 0.0, 0.0, 0.0), 'attractOut': (0.0, 0.0, 0.0, 0.0), 'segSplits': (0.0, 0.0, 0.0, 0.0)} +{'leafScale': 0.17000000178813934, 'autoTaper': True, 'customShape': (0.5, 1.0, 0.30000001192092896, 0.5), 'leafShape': 'hex', 'curve': (0.0, -40.0, -40.0, 0.0), 'ratio': 0.014999999664723873, 'splitBias': 0.0, 'pruneWidth': 0.4000000059604645, 'downAngleV': (0.0, 80.0, 10.0, 10.0), 'rotate': (99.5, 137.5, 137.5, 137.5), 'pruneRatio': 1.0, 'leafDownAngle': 45.0, 'makeMesh': False, 'radiusTweak': (1.0, 1.0, 1.0, 1.0), 'rMode': 'rotate', 'splitAngleV': (0.0, 0.0, 0.0, 0.0), 'branchDist': 1.0, 'bevel': True, 'minRadius': 0.001500000013038516, 'prune': False, 'leafRotateV': 0.0, 'splitAngle': (0.0, 0.0, 0.0, 0.0), 'armAnim': False, 'boneStep': (1, 1, 1, 1), 'pruneBase': 0.30000001192092896, 'taperCrown': 0.0, 'baseSplits': 0, 'baseSize_s': 0.25, 'handleType': '0', 'baseSize': 0.4000000059604645, 'af1': 1.0, 'levels': 2, 'leafScaleV': 0.0, 'resU': 4, 'seed': 0, 'downAngle': (90.0, 110.0, 45.0, 45.0), 'leafangle': 0.0, 'scaleV0': 0.10000000149011612, 'prunePowerHigh': 0.5, 'splitByLen': True, 'wind': 1.0, 'shape': '7', 'prunePowerLow': 0.0010000000474974513, 'scale': 13.0, 'leafAnim': False, 'curveBack': (0.0, 0.0, 0.0, 0.0), 'leafScaleX': 1.0, 'horzLeaves': True, 'splitHeight': 0.20000000298023224, 'leafScaleT': 0.0, 'scaleV': 3.0, 'leafDist': '6', 'nrings': 0, 'curveRes': (8, 5, 3, 1), 'shapeS': '4', 'bevelRes': 0, 'useOldDownAngle': False, 'useParentAngle': True, 'armLevels': 2, 'scale0': 1.0, 'taper': (1.0, 1.0, 1.0, 1.0), 'pruneWidthPeak': 0.6000000238418579, 'previewArm': False, 'leaves': 25, 'ratioPower': 1.100000023841858, 'gustF': 0.07500000298023224, 'curveV': (20.0, 50.0, 75.0, 0.0), 'showLeaves': False, 'frameRate': 1.0, 'length': (1.0, 0.30000001192092896, 0.6000000238418579, 0.44999998807907104), 'branches': (0, 50, 30, 10), 'useArm': False, 'loopFrames': 0, 'gust': 1.0, 'af3': 4.0, 'closeTip': False, 'leafRotate': 137.5, 'attractUp': (0.0, 0.0, 0.5, 0.5), 'leafDownAngleV': 10.0, 'rootFlare': 1.0, 'af2': 1.0, 'lengthV': (0.0, 0.0, 0.0, 0.0), 'rotateV': (15.0, 0.0, 0.0, 0.0), 'attractOut': (0.0, 0.0, 0.0, 0.0), 'segSplits': (0.0, 0.0, 0.0, 0.0)} diff --git a/add_curve_sapling/presets/willow.py b/add_curve_sapling/presets/willow.py index 5c60b0ff..3384eb19 100644 --- a/add_curve_sapling/presets/willow.py +++ b/add_curve_sapling/presets/willow.py @@ -1 +1 @@ -{'curveRes': (8, 16, 12, 1), 'scaleV0': 0.0, 'pruneRatio': 1.0, 'rotate': (0.0, -120.0, -120.0, 140.0), 'resU': 4, 'levels': 2, 'frameRate': 1.0, 'ratioPower': 2.0, 'windGust': 0.0, 'branches': (0, 25, 10, 300), 'attractUp': -3.0, 'bevel': False, 'windSpeed': 2.0, 'rotateV': (0.0, 30.0, 30.0, 0.0), 'segSplits': (0.10000000149011612, 0.20000000298023224, 0.20000000298023224, 0.0), 'handleType': '1', 'shape': '3', 'curveV': (120.0, 90.0, 0.0, 0.0), 'scale': 15.0, 'leafShape': 'hex', 'showLeaves': False, 'ratio': 0.029999999329447746, 'leaves': 15.0, 'armAnim': False, 'leafScale': 0.11999999731779099, 'leafDist': '4', 'useArm': False, 'splitAngle': (3.0, 30.0, 45.0, 0.0), 'lengthV': (0.0, 0.10000000149011612, 0.0, 0.0), 'seed': 0, 'scaleV': 5.0, 'startCurv': 0.0, 'downAngle': (0.0, 20.0, 30.0, 20.0), 'pruneWidth': 0.4000000059604645, 'baseSize': 0.05000000074505806, 'bevelRes': 0, 'length': (0.800000011920929, 0.5, 1.5, 0.10000000149011612), 'downAngleV': (0.0, 10.0, 10.0, 10.0), 'prune': False, 'curve': (0.0, 40.0, 0.0, 0.0), 'taper': (1.0, 1.0, 1.0, 1.0), 'prunePowerHigh': 0.5, 'leafScaleX': 0.20000000298023224, 'curveBack': (20.0, 80.0, 0.0, 0.0), 'bend': 0.0, 'scale0': 1.0, 'prunePowerLow': 0.0010000000474974513, 'splitAngleV': (0.0, 10.0, 20.0, 0.0), 'baseSplits': 2, 'pruneWidthPeak': 0.6000000238418579} +{'curveRes': (8, 16, 12, 1), 'scaleV0': 0.0, 'pruneRatio': 1.0, 'rotate': (0.0, -120.0, -120.0, 140.0), 'resU': 4, 'levels': 2, 'frameRate': 1.0, 'ratioPower': 2.0, 'windGust': 0.0, 'branches': (0, 25, 10, 300), 'attractUp': -3.0, 'bevel': True, 'windSpeed': 2.0, 'rotateV': (0.0, 30.0, 30.0, 0.0), 'segSplits': (0.10000000149011612, 0.20000000298023224, 0.20000000298023224, 0.0), 'handleType': '1', 'shape': '3', 'curveV': (120.0, 90.0, 0.0, 0.0), 'scale': 15.0, 'leafShape': 'hex', 'showLeaves': False, 'ratio': 0.029999999329447746, 'leaves': 15.0, 'armAnim': False, 'leafScale': 0.11999999731779099, 'leafDist': '4', 'useArm': False, 'splitAngle': (3.0, 30.0, 45.0, 0.0), 'lengthV': (0.0, 0.10000000149011612, 0.0, 0.0), 'seed': 0, 'scaleV': 5.0, 'startCurv': 0.0, 'downAngle': (0.0, 20.0, 30.0, 20.0), 'pruneWidth': 0.4000000059604645, 'baseSize': 0.05000000074505806, 'bevelRes': 0, 'length': (0.800000011920929, 0.5, 1.5, 0.10000000149011612), 'downAngleV': (0.0, 10.0, 10.0, 10.0), 'prune': False, 'curve': (0.0, 40.0, 0.0, 0.0), 'taper': (1.0, 1.0, 1.0, 1.0), 'prunePowerHigh': 0.5, 'leafScaleX': 0.20000000298023224, 'curveBack': (20.0, 80.0, 0.0, 0.0), 'bend': 0.0, 'scale0': 1.0, 'prunePowerLow': 0.0010000000474974513, 'splitAngleV': (0.0, 10.0, 20.0, 0.0), 'baseSplits': 2, 'pruneWidthPeak': 0.6000000238418579} -- cgit v1.2.3 From e334332a03b5a588ebc3c6473cf39c4515bdc9e6 Mon Sep 17 00:00:00 2001 From: CansecoGPC Date: Thu, 16 Jul 2020 17:53:00 +0200 Subject: Sapling Tree Gen: Fix T77949 by removing comments around bend code. --- add_curve_sapling/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/add_curve_sapling/__init__.py b/add_curve_sapling/__init__.py index b12beada..e5b88157 100644 --- a/add_curve_sapling/__init__.py +++ b/add_curve_sapling/__init__.py @@ -701,7 +701,6 @@ class AddTree(Operator): items=objectList, update=update_leaves ) - """ bend = FloatProperty( name='Leaf Bend', description='The proportion of bending applied to the leaf (Bend)', @@ -709,7 +708,6 @@ class AddTree(Operator): max=1.0, default=0.0, update=update_leaves ) - """ leafangle: FloatProperty( name='Leaf Angle', description='Leaf vertical attraction', -- cgit v1.2.3 From 49bae5d692b9c8bfe0bacbc8a35ffe6e0105524a Mon Sep 17 00:00:00 2001 From: Samuli Riihikoski Date: Fri, 17 Jul 2020 22:14:53 +0300 Subject: io_coat3D: update version number --- io_coat3D/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_coat3D/__init__.py b/io_coat3D/__init__.py index 4f79557c..4aebee06 100644 --- a/io_coat3D/__init__.py +++ b/io_coat3D/__init__.py @@ -19,7 +19,7 @@ bl_info = { "name": "3D-Coat Applink", "author": "Kalle-Samuli Riihikoski (haikalle)", - "version": (4, 9, 34), + "version": (4, 9, 50), "blender": (2, 80, 0), "location": "Scene > 3D-Coat Applink", "description": "Transfer data between 3D-Coat/Blender", -- cgit v1.2.3 From bda1b8bc98bc0315f1090c131fab19a3b282e047 Mon Sep 17 00:00:00 2001 From: Samuli Date: Sat, 18 Jul 2020 00:08:57 +0300 Subject: io_coat3D: adding many features and fixes --- io_coat3D/__init__.py | 185 ++++++----- io_coat3D/data.json | 9 - io_coat3D/tex.py | 306 +++++------------- io_coat3D/texVR.py | 846 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1040 insertions(+), 306 deletions(-) create mode 100644 io_coat3D/texVR.py diff --git a/io_coat3D/__init__.py b/io_coat3D/__init__.py index 4aebee06..5d74a960 100644 --- a/io_coat3D/__init__.py +++ b/io_coat3D/__init__.py @@ -19,7 +19,7 @@ bl_info = { "name": "3D-Coat Applink", "author": "Kalle-Samuli Riihikoski (haikalle)", - "version": (4, 9, 50), + "version": (4, 9, 34), "blender": (2, 80, 0), "location": "Scene > 3D-Coat Applink", "description": "Transfer data between 3D-Coat/Blender", @@ -36,7 +36,7 @@ else: from bpy.app.handlers import persistent from io_coat3D import tex -#from io_coat3D import texVR +from io_coat3D import texVR import os import ntpath @@ -67,57 +67,6 @@ time_interval = 2.0 global_exchange_folder = '' - - -@persistent -def every_3_seconds(): - global global_exchange_folder - global initial_settings - path_ex = '' - - if(initial_settings): - global_exchange_folder = set_exchange_folder() - initial_settings = False - - coat3D = bpy.context.scene.coat3D - - Export_folder = global_exchange_folder - Blender_folder = os.path.join(Export_folder, 'Blender') - - BlenderFolder = Blender_folder - ExportFolder = Export_folder - - Blender_folder += ('%sexport.txt' % (os.sep)) - Export_folder += ('%sexport.txt' % (os.sep)) - - - if os.path.isfile(Export_folder): - - print('BLENDER -> 3DC -> BLENDER WORKFLLOW') - DeleteExtra3DC() - workflow1(ExportFolder) - removeFile(Export_folder) - removeFile(Blender_folder) - - - - elif os.path.isfile(Blender_folder): - - print('3DC -> BLENDER WORKFLLOW') - DeleteExtra3DC() - workflow2(BlenderFolder) - removeFile(Blender_folder) - - - - - return 3.0 - -@persistent -def load_handler(dummy): - bpy.app.timers.register(every_3_seconds) - - def removeFile(exportfile): if (os.path.isfile(exportfile)): os.remove(exportfile) @@ -155,19 +104,44 @@ def set_exchange_folder(): if(os.path.isdir(exchange)): bpy.coat3D['status'] = 1 + if(platform == 'win32'): + exchange_path = os.path.expanduser("~") + os.sep + 'Documents' + os.sep + '3DC2Blender' + os.sep + 'Exchange_folder.txt' applink_folder = os.path.expanduser("~") + os.sep + 'Documents' + os.sep + '3DC2Blender' if(not(os.path.isdir(applink_folder))): os.makedirs(applink_folder) + else: + exchange_path = os.path.expanduser("~") + os.sep + 'Documents' + os.sep + '3DC2Blender' + os.sep + 'Exchange_folder.txt' applink_folder = os.path.expanduser("~") + os.sep + 'Documents' + os.sep + '3DC2Blender' if(not(os.path.isdir(applink_folder))): os.makedirs(applink_folder) - file = open(exchange_path, "w") - file.write("%s"%(coat3D.exchangedir)) - file.close() + + if(os.path.isfile(exchange_path) == False): + + file = open(exchange_path, "w") + file.write("%s"%(exchange_path)) + file.close() + + else: + + exchangeline = open(exchange_path) + for line in exchangeline: + source = line + break + exchangeline.close() + + if(source != coat3D.exchangedir and coat3D.exchangedir != '' and coat3D.exchangedir.rfind('Exchange') >= 0): + + file = open(exchange_path, "w") + file.write("%s"%(coat3D.exchangedir)) + file.close() + exchange = coat3D.exchangedir + + else: + exchange = source else: if(platform == 'win32'): @@ -300,7 +274,6 @@ def updatemesh(objekti, proxy, texturelist): vertex_map_copy.data[loop_index].color = proxy.data.vertex_colors[0].data[loop_index].color # UV -Sets - udim_textures = False if(texturelist != []): if(texturelist[0][0].startswith('100')): @@ -311,7 +284,7 @@ def updatemesh(objekti, proxy, texturelist): uv_count = len(proxy.data.uv_layers) index = 0 - while(index < uv_count): + while(index < uv_count and len(proxy.data.polygons) == len(objekti.data.polygons)): for poly in proxy.data.polygons: for indi in poly.loop_indices: if(proxy.data.uv_layers[index].data[indi].uv[0] != 0 and proxy.data.uv_layers[index].data[indi].uv[1] != 0): @@ -319,11 +292,10 @@ def updatemesh(objekti, proxy, texturelist): if(udim_textures): udim = proxy.data.uv_layers[index].name udim_index = int(udim[2:]) - 1 - + objekti.data.uv_layers[0].data[indi].uv[0] = proxy.data.uv_layers[index].data[indi].uv[0] objekti.data.uv_layers[0].data[indi].uv[1] = proxy.data.uv_layers[index].data[indi].uv[1] - if(udim_textures): - objekti.data.uv_layers[0].data[indi].uv[0] += udim_index + index = index + 1 # Mesh Copy @@ -333,6 +305,54 @@ def updatemesh(objekti, proxy, texturelist): for ind, v in enumerate(objekti.data.vertices): v.co = proxy.data.vertices[ind].co +class SCENE_OT_getback(bpy.types.Operator): + bl_idname = "getback.pilgway_3d_coat" + bl_label = "Export your custom property" + bl_description = "Export your custom property" + bl_options = {'UNDO'} + + def invoke(self, context, event): + + global global_exchange_folder + global initial_settings + path_ex = '' + + if(initial_settings): + global_exchange_folder = set_exchange_folder() + initial_settings = False + + Export_folder = global_exchange_folder + Blender_folder = os.path.join(Export_folder, 'Blender') + + BlenderFolder = Blender_folder + ExportFolder = Export_folder + + Blender_folder += ('%sexport.txt' % (os.sep)) + Export_folder += ('%sexport.txt' % (os.sep)) + + if (bpy.app.background == False): + if os.path.isfile(Export_folder): + + print('BLENDER -> 3DC -> BLENDER WORKFLLOW') + DeleteExtra3DC() + workflow1(ExportFolder) + removeFile(Export_folder) + removeFile(Blender_folder) + + + + elif os.path.isfile(Blender_folder): + + print('3DC -> BLENDER WORKFLLOW') + DeleteExtra3DC() + workflow2(BlenderFolder) + removeFile(Blender_folder) + + + + return {'FINISHED'} + + class SCENE_OT_folder(bpy.types.Operator): bl_idname = "update_exchange_folder.pilgway_3d_coat" bl_label = "Export your custom property" @@ -760,11 +780,11 @@ class SCENE_OT_export(bpy.types.Operator): if(coat3D.type == 'autopo'): coat3D.bring_retopo = True coat3D.bring_retopo_path = checkname - bpy.ops.export_scene.fbx(filepath=checkname, use_selection=True, use_mesh_modifiers=coat3D.exportmod, axis_forward='-Z', axis_up='Y') + bpy.ops.export_scene.fbx(filepath=checkname, global_scale = 0.01, use_selection=True, use_mesh_modifiers=coat3D.exportmod, axis_forward='-Z', axis_up='Y') elif (coat3D.type == 'vox'): coat3D.bring_retopo = False - bpy.ops.export_scene.fbx(filepath=coa.applink_address, global_scale=1, use_selection=True, + bpy.ops.export_scene.fbx(filepath=coa.applink_address, global_scale = 0.01, use_selection=True, use_mesh_modifiers=coat3D.exportmod, axis_forward='-Z', axis_up='Y') else: @@ -803,11 +823,12 @@ class SCENE_OT_export(bpy.types.Operator): if(node.name.startswith('3DC_') == True): material.material.node_tree.nodes.remove(node) - + for ind, mat_list in enumerate(mod_mat_list): - if(mat_list == objekti.name): + if(mat_list == '__' + objekti.name): for ind, mat in enumerate(mod_mat_list[mat_list]): objekti.material_slots[mod_mat_list[mat_list][ind][0]].material = mod_mat_list[mat_list][ind][1] + bpy.context.scene.render.engine = active_render return {'FINISHED'} @@ -906,10 +927,14 @@ def new_ref_function(new_applink_address, nimi): refmesh.coat3D.applink_name = '' refmesh.coat3D.applink_address = '' refmesh.coat3D.type = '' + copymesh.scale = (1,1,1) + copymesh.coat3D.applink_scale = (1,1,1) + copymesh.location = (0,0,0) + copymesh.rotation_euler = (0,0,0) def blender_3DC_blender(texturelist): - + coat3D = bpy.context.scene.coat3D old_materials = bpy.data.materials.keys() @@ -965,7 +990,6 @@ def blender_3DC_blender(texturelist): for oname in object_list: objekti = bpy.data.objects[oname] - if(objekti.coat3D.applink_mesh == True): path3b_n = coat3D.exchangedir @@ -1034,7 +1058,7 @@ def blender_3DC_blender(texturelist): if objekti.coat3D.applink_firsttime == True and objekti.coat3D.type == 'vox': objekti.select_set(True) - objekti.scale = (1, 1, 1) + objekti.scale = (0.01, 0.01, 0.01) objekti.rotation_euler[0] = 1.5708 objekti.rotation_euler[2] = 1.5708 bpy.ops.object.transforms_to_deltas(mode='ROT') @@ -1062,8 +1086,7 @@ def blender_3DC_blender(texturelist): #delete_materials_from_end(keep_materials_count, obj_proxy) - for index, material in enumerate(objekti.material_slots): - obj_proxy.material_slots[index-1].material = material.material + updatemesh(objekti,obj_proxy, texturelist) bpy.context.view_layer.objects.active = objekti @@ -1247,6 +1270,7 @@ def blender_3DC(texturelist, new_applink_address): if(facture_object): texVR.matlab(new_obj, mat_list, texturelist, is_new) + new_obj.scale = (0.01, 0.01, 0.01) else: tex.matlab(new_obj, mat_list, texturelist, is_new) @@ -1354,7 +1378,6 @@ def workflow2(BlenderFolder): new_ref_object = False if(os.path.isfile(Blender_export)): - print('blender') obj_pathh = open(Blender_export) new_object = True for line in obj_pathh: @@ -1420,9 +1443,10 @@ class SCENE_PT_Main(bpy.types.Panel): row.prop(coat3D,"type",text = "") flow = layout.grid_flow(row_major=True, columns=0, even_columns=False, even_rows=False, align=True) - col = flow.column() + row = layout.row() - col.operator("export_applink.pilgway_3d_coat", text="Send") + row.operator("export_applink.pilgway_3d_coat", text="Send") + row.operator("getback.pilgway_3d_coat", text="GetBack") class ObjectButtonsPanel(): @@ -1944,6 +1968,11 @@ class MaterialCoat3D(PropertyGroup): description="Import diffuse texture", default=True ) + bring_gloss: BoolProperty( + name="Import diffuse texture", + description="Import diffuse texture", + default=True + ) classes = ( SCENE_PT_Main, @@ -1957,6 +1986,7 @@ classes = ( SCENE_OT_folder, SCENE_OT_opencoat, SCENE_OT_export, + SCENE_OT_getback, SCENE_OT_delete_material_nodes, SCENE_OT_delete_object_nodes, SCENE_OT_delete_collection_nodes, @@ -2014,6 +2044,11 @@ def register(): description="Import alpha texture", default=True ) + bpy.types.Material.coat3D_gloss = BoolProperty( + name="Import alpha texture", + description="Import alpha texture", + default=True + ) from bpy.utils import register_class @@ -2023,9 +2058,7 @@ def register(): bpy.types.Object.coat3D = PointerProperty(type=ObjectCoat3D) bpy.types.Scene.coat3D = PointerProperty(type=SceneCoat3D) bpy.types.Mesh.coat3D = PointerProperty(type=MeshCoat3D) - bpy.types.Material.coat3D = PointerProperty(type=MaterialCoat3D) - bpy.app.handlers.load_post.append(load_handler) - + bpy.types.Material.coat3D = PointerProperty(type=MaterialCoat3D) kc = bpy.context.window_manager.keyconfigs.addon diff --git a/io_coat3D/data.json b/io_coat3D/data.json index 4eaf03e8..8ed0b54e 100644 --- a/io_coat3D/data.json +++ b/io_coat3D/data.json @@ -9,7 +9,6 @@ "input": 0, "rampnode": "no", "huenode": "yes", - "node_location": [ -400, 400 ], "node_color": [0.535, 0.608, 0.306] }, @@ -22,7 +21,6 @@ "input": 1, "rampnode": "yes", "huenode": "no", - "node_location": [ -830, 160 ], "node_color": [ 0.027, 0.324, 0.908 ] }, @@ -35,7 +33,6 @@ "input": 2, "rampnode": "yes", "huenode": "no", - "node_location": [ -550, 0 ], "node_color": [ 0.608, 0.254, 0.000 ] }, @@ -48,7 +45,6 @@ "input": 3, "rampnode": "no", "huenode": "no", - "node_location": [ -650, -500 ], "normal_node_location": [ -350, -350 ], "node_color": [ 0.417, 0.363, 0.608 ] }, @@ -60,7 +56,6 @@ "displacement": "yes", "rampnode": "yes", "huenode": "no", - "node_location": [ 100, 100 ], "input": 5, "node_color": [ 0.608, 0.591, 0.498 ] }, @@ -73,7 +68,6 @@ "find_input": [ "Emissive" ], "rampnode": "no", "huenode": "yes", - "node_location": [ 100, 100 ], "input": 4, "node_color": [ 0.0, 0.0, 0.0 ] }, @@ -85,7 +79,6 @@ "displacement": "no", "rampnode": "no", "huenode": "no", - "node_location": [ 100, 100 ], "node_color": [ 0.0, 0.0, 0.0 ] }, @@ -96,7 +89,6 @@ "displacement": "no", "rampnode": "yes", "huenode": "no", - "node_location": [ 100, 100 ], "node_color": [ 0.1, 0.5, 0.1 ] }, @@ -109,7 +101,6 @@ "input": 8, "rampnode": "no", "huenode": "no", - "node_location": [ 0, -1000 ], "node_color": [0.535, 0.608, 0.306] } diff --git a/io_coat3D/tex.py b/io_coat3D/tex.py index 95dcf59c..3a48ab8f 100644 --- a/io_coat3D/tex.py +++ b/io_coat3D/tex.py @@ -108,43 +108,23 @@ def updatetextures(objekti): # Update 3DC textures for node in index_mat.material.node_tree.nodes: if (node.type == 'TEX_IMAGE'): - if (node.name == '3DC_color'): - node.image.reload() - elif (node.name == '3DC_metalness'): - node.image.reload() - elif (node.name == '3DC_rough'): - node.image.reload() - elif (node.name == '3DC_nmap'): - node.image.reload() - elif (node.name == '3DC_displacement'): - node.image.reload() - elif (node.name == '3DC_emissive'): - node.image.reload() - elif (node.name == '3DC_AO'): - node.image.reload() - elif (node.name == '3DC_alpha'): - node.image.reload() + if (node.name == '3DC_color' or node.name == '3DC_metalness' or node.name == '3DC_rough' or node.name == '3DC_nmap' + or node.name == '3DC_displacement' or node.name == '3DC_emissive' or node.name == '3DC_AO' or node.name == '3DC_alpha'): + try: + node.image.reload() + except: + pass for index_node_group in bpy.data.node_groups: for node in index_node_group.nodes: if (node.type == 'TEX_IMAGE'): - if (node.name == '3DC_color'): - node.image.reload() - elif (node.name == '3DC_metalness'): - node.image.reload() - elif (node.name == '3DC_rough'): - node.image.reload() - elif (node.name == '3DC_nmap'): - node.image.reload() - elif (node.name == '3DC_displacement'): - node.image.reload() - elif (node.name == '3DC_emissive'): - node.image.reload() - elif (node.name == '3DC_AO'): - node.image.reload() - elif (node.name == '3DC_alpha'): - node.image.reload() + if (node.name == '3DC_color' or node.name == '3DC_metalness' or node.name == '3DC_rough' or node.name == '3DC_nmap' + or node.name == '3DC_displacement' or node.name == '3DC_emissive' or node.name == '3DC_AO' or node.name == '3DC_alpha'): + try: + node.image.reload() + except: + pass def testi(objekti, texture_info, index_mat_name, uv_MODE_mat, mat_index): if uv_MODE_mat == 'UV': @@ -173,11 +153,6 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r create_nodes = False for ind, index_mat in enumerate(objekti.material_slots): - if(udim_textures): - tile_list = UVTiling(objekti,ind, texturelist) - else: - tile_list = [] - texcoat = {} texcoat['color'] = [] texcoat['ao'] = [] @@ -272,36 +247,45 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r for texture_info in texturelist: if texture_info[2] == 'color' or texture_info[2] == 'diffuse': - texcoat['color'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['color'] == [] and texture_info[1] == '1001': + texcoat['color'].append(texture_info[3]) + create_nodes = True elif texture_info[2] == 'metalness' or texture_info[2] == 'specular' or texture_info[ 2] == 'reflection': - texcoat['metalness'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['metalness'] == [] and texture_info[1] == '1001': + texcoat['metalness'].append(texture_info[3]) + create_nodes = True elif texture_info[2] == 'rough' or texture_info[2] == 'roughness': - texcoat['rough'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['rough'] == [] and texture_info[1] == '1001': + texcoat['rough'].append(texture_info[3]) + create_nodes = True elif texture_info[2] == 'nmap' or texture_info[2] == 'normalmap' or texture_info[ 2] == 'normal_map' or texture_info[2] == 'normal': - texcoat['nmap'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['nmap'] == [] and texture_info[1] == '1001': + texcoat['nmap'].append(texture_info[3]) + create_nodes = True elif texture_info[2] == 'emissive': - texcoat['emissive'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['emissive'] == [] and texture_info[1] == '1001': + texcoat['emissive'].append(texture_info[3]) + create_nodes = True elif texture_info[2] == 'emissive_power': - texcoat['emissive_power'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['emissive_power'] == [] and texture_info[1] == '1001': + texcoat['emissive_power'].append(texture_info[3]) + create_nodes = True elif texture_info[2] == 'ao': - texcoat['ao'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['ao'] == [] and texture_info[1] == '1001': + texcoat['ao'].append(texture_info[3]) + create_nodes = True elif texture_info[2].startswith('displacement'): - texcoat['displacement'].append([texture_info[0],texture_info[3]]) - create_nodes = True + if texcoat['displacement'] == [] and texture_info[1] == '1001': + texcoat['displacement'].append(texture_info[3]) + create_nodes = True if texture_info[2] == 'alpha' or texture_info[2] == 'opacity': - texcoat['alpha'].append([texture_info[0], texture_info[3]]) - create_nodes = True + if texcoat['alpha'] == [] and texture_info[1] == '1001': + texcoat['alpha'].append(texture_info[3]) + create_nodes = True create_group_node = True - + if(create_nodes): coat3D = bpy.context.scene.coat3D path3b_n = coat3D.exchangedir @@ -313,9 +297,9 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r objekti.coat3D.applink_3b_path = line export_file.close() coat3D.remove_path = True - createnodes(index_mat, texcoat, create_group_node, tile_list, objekti, ind, is_new) + createnodes(index_mat, texcoat, create_group_node, objekti, ind, is_new, udim_textures) -def createnodes(active_mat,texcoat, create_group_node, tile_list, objekti, ind, is_new): # Creates new nodes and link textures into them +def createnodes(active_mat,texcoat, create_group_node, objekti, ind, is_new, udim_textures): # Creates new nodes and link textures into them bring_color = True # Meaning of these is to check if we can only update textures or do we need to create new nodes bring_metalness = True bring_roughness = True @@ -325,9 +309,13 @@ def createnodes(active_mat,texcoat, create_group_node, tile_list, objekti, ind, bring_AO = True bring_alpha = True + active_mat.material.show_transparent_back = False # HACK FOR BLENDER BUG + coat3D = bpy.context.scene.coat3D coatMat = active_mat.material + coatMat.blend_method = 'BLEND' + if(coatMat.use_nodes == False): coatMat.use_nodes = True act_material = coatMat.node_tree @@ -456,171 +444,43 @@ def createnodes(active_mat,texcoat, create_group_node, tile_list, objekti, ind, if(out_mat.inputs['Surface'].is_linked == True): if(bring_color == True and texcoat['color'] != []): CreateTextureLine(data['color'], act_material, main_mat, texcoat, coat3D, notegroup, - main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures) if(bring_metalness == True and texcoat['metalness'] != []): CreateTextureLine(data['metalness'], act_material, main_mat, texcoat, coat3D, notegroup, - main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures) if(bring_roughness == True and texcoat['rough'] != []): CreateTextureLine(data['rough'], act_material, main_mat, texcoat, coat3D, notegroup, - main_material, applink_tree, out_mat, coatMat,tile_list, objekti, ind, is_new) + main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures) if(bring_normal == True and texcoat['nmap'] != []): CreateTextureLine(data['nmap'], act_material, main_mat, texcoat, coat3D, notegroup, - main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures) if (bring_emissive == True and texcoat['emissive'] != []): CreateTextureLine(data['emissive'], act_material, main_mat, texcoat, coat3D, notegroup, - main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures) if (bring_displacement == True and texcoat['displacement'] != []): CreateTextureLine(data['displacement'], act_material, main_mat, texcoat, coat3D, notegroup, - main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures) if (bring_alpha == True and texcoat['alpha'] != []): CreateTextureLine(data['alpha'], act_material, main_mat, texcoat, coat3D, notegroup, - main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) - - -def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new): - - if(tile_list): - texture_name = coatMat.name + '_' + type['name'] - texture_tree = bpy.data.node_groups.new(type="ShaderNodeTree", name=texture_name) - texture_tree.outputs.new("NodeSocketColor", "Color") - texture_tree.outputs.new("NodeSocketColor", "Alpha") - texture_node_tree = act_material.nodes.new('ShaderNodeGroup') - texture_node_tree.name = '3DC_' + type['name'] - texture_node_tree.node_tree = texture_tree - texture_node_tree.location[0] = type['node_location'][0] - texture_node_tree.location[0] -= 400 - texture_node_tree.location[1] = type['node_location'][1] - notegroupend = texture_tree.nodes.new('NodeGroupOutput') - - count = len(tile_list) - uv_loc = [-1400, 200] - map_loc = [-1100, 200] - tex_loc = [-700, 200] - mix_loc = [-400, 100] - - nodes = [] - - for index, tile in enumerate(tile_list): - - tex_img_node = texture_tree.nodes.new('ShaderNodeTexImage') - - for ind, tex_index in enumerate(texcoat[type['name']]): - if(tex_index[0] == tile): - tex_img_node.image = bpy.data.images.load(texcoat[type['name']][ind][1]) - break - tex_img_node.location = tex_loc - - if tex_img_node.image and type['colorspace'] != 'color': - tex_img_node.image.colorspace_settings.is_data = True + main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures) - tex_uv_node = texture_tree.nodes.new('ShaderNodeUVMap') - tex_uv_node.location = uv_loc - if(is_new): - tex_uv_node.uv_map = objekti.data.uv_layers[ind].name - else: - tex_uv_node.uv_map = objekti.data.uv_layers[0].name - - map_node = texture_tree.nodes.new('ShaderNodeMapping') - map_node.location = map_loc - map_node.name = '3DC_' + tile - map_node.vector_type = 'TEXTURE' - - tile_int_x = int(tile[3]) - tile_int_y = int(tile[2]) - - min_node = texture_tree.nodes.new('ShaderNodeVectorMath') - min_node.operation = "MINIMUM" - min_node.inputs[1].default_value[0] = tile_int_x - 1 - min_node.inputs[1].default_value[1] = tile_int_y - - max_node = texture_tree.nodes.new('ShaderNodeVectorMath') - max_node.operation = "MAXIMUM" - max_node.inputs[1].default_value[0] = tile_int_x - max_node.inputs[1].default_value[1] = tile_int_y + 1 - - - if(index == 0): - nodes.append(tex_img_node.name) - if(count == 1): - texture_tree.links.new(tex_img_node.outputs[0], notegroupend.inputs[0]) - texture_tree.links.new(tex_img_node.outputs[1], notegroupend.inputs[1]) - - if(index == 1): - mix_node = texture_tree.nodes.new('ShaderNodeMixRGB') - mix_node.blend_type = 'ADD' - mix_node.inputs[0].default_value = 1 - mix_node.location = mix_loc - mix_loc[1] -= 300 - texture_tree.links.new(tex_img_node.outputs[0], mix_node.inputs[2]) - texture_tree.links.new(texture_tree.nodes[nodes[0]].outputs[0], mix_node.inputs[1]) - mix_node_alpha = texture_tree.nodes.new('ShaderNodeMath') - mix_node_alpha.location = mix_loc - mix_loc[1] -= 200 - texture_tree.links.new(tex_img_node.outputs[1], mix_node_alpha.inputs[1]) - texture_tree.links.new(texture_tree.nodes[nodes[0]].outputs[1], mix_node_alpha.inputs[0]) - nodes.clear() - nodes.append(tex_img_node.name) - nodes.append(mix_node.name) - nodes.append(mix_node_alpha.name) - - - elif(index > 1): - mix_node = texture_tree.nodes.new('ShaderNodeMixRGB') - mix_node.blend_type = 'ADD' - mix_node.inputs[0].default_value = 1 - mix_node.location = mix_loc - mix_loc[1] -= 300 - texture_tree.links.new(texture_tree.nodes[nodes[1]].outputs[0], mix_node.inputs[1]) - texture_tree.links.new(tex_img_node.outputs[0], mix_node.inputs[2]) - mix_node_alpha = texture_tree.nodes.new('ShaderNodeMath') - mix_node_alpha.location = mix_loc - mix_loc[1] -= 200 - texture_tree.links.new(texture_tree.nodes[nodes[2]].outputs[0], mix_node_alpha.inputs[0]) - texture_tree.links.new(tex_img_node.outputs[1], mix_node_alpha.inputs[1]) - - nodes.clear() - nodes.append(tex_img_node.name) - nodes.append(mix_node.name) - nodes.append(mix_node_alpha.name) - - tex_loc[1] -= 300 - uv_loc[1] -= 300 - map_loc[1] -= 300 - - texture_tree.links.new(tex_uv_node.outputs[0], map_node.inputs[0]) - texture_tree.links.new(map_node.outputs[0], min_node.inputs[0]) - texture_tree.links.new(min_node.outputs['Vector'], max_node.inputs[0]) - texture_tree.links.new(max_node.outputs['Vector'], tex_img_node.inputs[0]) - - if(count > 1): - texture_tree.links.new(mix_node.outputs[0], notegroupend.inputs[0]) - texture_tree.links.new(mix_node_alpha.outputs[0], notegroupend.inputs[1]) - - if(tile_list): - node = texture_node_tree - if(texcoat['alpha'] != []): - if (type['name'] == 'color'): - act_material.links.new(node.outputs[1], notegroup.inputs[8]) - else: - if(type['name'] == 'alpha'): - act_material.links.new(node.outputs[1], notegroup.inputs[8]) +def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, main_material, applink_tree, out_mat, coatMat, objekti, ind, is_new, udim_textures): + node = act_material.nodes.new('ShaderNodeTexImage') + uv_node = act_material.nodes.new('ShaderNodeUVMap') + if (is_new): + uv_node.uv_map = objekti.data.uv_layers[ind].name else: - node = act_material.nodes.new('ShaderNodeTexImage') - uv_node = act_material.nodes.new('ShaderNodeUVMap') - if (is_new): - uv_node.uv_map = objekti.data.uv_layers[ind].name - else: - uv_node.uv_map = objekti.data.uv_layers[0].name - act_material.links.new(uv_node.outputs[0], node.inputs[0]) - uv_node.use_custom_color = True - uv_node.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + uv_node.uv_map = objekti.data.uv_layers[0].name + act_material.links.new(uv_node.outputs[0], node.inputs[0]) + uv_node.use_custom_color = True + uv_node.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) node.use_custom_color = True node.color = (type['node_color'][0],type['node_color'][1],type['node_color'][2]) @@ -632,8 +492,7 @@ def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, normal_node.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) node.location = -671, -510 - if(tile_list == []): - uv_node.location = -750, -600 + uv_node.location = -750, -600 normal_node.location = -350, -350 normal_node.name = '3DC_normalnode' @@ -654,21 +513,26 @@ def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, if(input_color != -1): break - if (tile_list == []): - - load_image = True + load_image = True - for image in bpy.data.images: - if(texcoat[type['name']][0] == image.filepath): - load_image = False - node.image = image - break + for image in bpy.data.images: + + if(texcoat[type['name']][0] == image.filepath): + load_image = False + node.image = image + + break - if (load_image): - node.image = bpy.data.images.load(texcoat[type['name']][0]) + if (load_image): + print('load_image', texcoat[type['name']][0]) - if node.image and type['colorspace'] == 'noncolor': - node.image.colorspace_settings.is_data = True + node.image = bpy.data.images.load(texcoat[type['name']][0]) + if(udim_textures): + node.image.source = 'TILED' + + + if node.image and type['colorspace'] == 'noncolor': + node.image.colorspace_settings.is_data = True if (coat3D.createnodes): @@ -725,10 +589,10 @@ def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, if(material.name == '3DC_Emission'): main_material.links.new(applink_tree.outputs[type['input']], material.inputs[0]) break - if(tile_list == []): - uv_node.location = node.location - uv_node.location[0] -= 300 - uv_node.location[1] -= 200 + + uv_node.location = node.location + uv_node.location[0] -= 300 + uv_node.location[1] -= 200 else: node.location = type['node_location'][0], type['node_location'][1] @@ -826,7 +690,7 @@ def matlab(objekti,mat_list,texturelist,is_new): if(texturelist != []): udim_textures = False - if texturelist[0][0].startswith('100'): + if texturelist[0][0].startswith('100') and len(texturelist[0][0]) == 4: udim_textures = True if(udim_textures == False): diff --git a/io_coat3D/texVR.py b/io_coat3D/texVR.py new file mode 100644 index 00000000..9f0e65ec --- /dev/null +++ b/io_coat3D/texVR.py @@ -0,0 +1,846 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# 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 2 +# 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 +# MERCHANTABILITY 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, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +import bpy +import os +import re +import json + +def find_index(objekti): + + luku = 0 + for tex in objekti.active_material.texture_slots: + if(not(hasattr(tex,'texture'))): + break + luku = luku +1 + return luku + + +def RemoveFbxNodes(objekti): + Node_Tree = objekti.active_material.node_tree + for node in Node_Tree.nodes: + if node.type != 'OUTPUT_MATERIAL': + Node_Tree.nodes.remove(node) + else: + output = node + output.location = 340,400 + Prin_mat = Node_Tree.nodes.new(type="ShaderNodeBsdfPrincipled") + Prin_mat.location = 13, 375 + + Node_Tree.links.new(Prin_mat.outputs[0], output.inputs[0]) + + +def UVTiling(objekti, index, texturelist): + """ Checks what Tiles are linked with Material """ + + objekti.coat3D.applink_scale = objekti.scale + tiles_index = [] + tile_number ='' + for poly in objekti.data.polygons: + if (poly.material_index == (index)): + loop_index = poly.loop_indices[0] + uv_x = objekti.data.uv_layers.active.data[loop_index].uv[0] + if(uv_x >= 0 and uv_x <=1): + tile_number_x = '1' + elif (uv_x >= 1 and uv_x <= 2): + tile_number_x = '2' + elif (uv_x >= 2 and uv_x <= 3): + tile_number_x = '3' + elif (uv_x >= 3 and uv_x <= 4): + tile_number_x = '4' + elif (uv_x >= 4 and uv_x <= 5): + tile_number_x = '5' + elif (uv_x >= 5 and uv_x <= 6): + tile_number_x = '6' + elif (uv_x >= 6 and uv_x <= 7): + tile_number_x = '7' + elif (uv_x >= 7 and uv_x <= 8): + tile_number_x = '8' + elif (uv_x >= 8 and uv_x <= 9): + tile_number_x = '9' + + uv_y = objekti.data.uv_layers.active.data[loop_index].uv[1] + if (uv_y >= 0 and uv_y <= 1): + tile_number_y = '0' + elif (uv_y >= 1 and uv_y <= 2): + tile_number_y = '1' + elif (uv_x >= 2 and uv_y <= 3): + tile_number_y = '2' + elif (uv_x >= 3 and uv_y <= 4): + tile_number_y = '3' + elif (uv_x >= 4 and uv_y <= 5): + tile_number_y = '4' + elif (uv_x >= 5 and uv_y <= 6): + tile_number_y = '5' + elif (uv_x >= 6 and uv_y <= 7): + tile_number_y = '6' + elif (uv_x >= 7 and uv_y <= 8): + tile_number_y = '7' + elif (uv_x >= 8 and uv_y <= 9): + tile_number_y = '8' + + tile_number = '10' + tile_number_y + tile_number_x + + if tile_number not in tiles_index: + tiles_index.append(tile_number) + + return tiles_index + +def updatetextures(objekti): # Update 3DC textures + + for index_mat in objekti.material_slots: + + for node in index_mat.material.node_tree.nodes: + if (node.type == 'TEX_IMAGE'): + if (node.name == '3DC_color'): + node.image.reload() + elif (node.name == '3DC_metalness'): + node.image.reload() + elif (node.name == '3DC_rough'): + node.image.reload() + elif (node.name == '3DC_nmap'): + node.image.reload() + elif (node.name == '3DC_displacement'): + node.image.reload() + elif (node.name == '3DC_emissive'): + node.image.reload() + elif (node.name == '3DC_AO'): + node.image.reload() + elif (node.name == '3DC_alpha'): + node.image.reload() + + for index_node_group in bpy.data.node_groups: + + for node in index_node_group.nodes: + if (node.type == 'TEX_IMAGE'): + if (node.name == '3DC_color'): + node.image.reload() + elif (node.name == '3DC_metalness'): + node.image.reload() + elif (node.name == '3DC_rough'): + node.image.reload() + elif (node.name == '3DC_nmap'): + node.image.reload() + elif (node.name == '3DC_displacement'): + node.image.reload() + elif (node.name == '3DC_emissive'): + node.image.reload() + elif (node.name == '3DC_AO'): + node.image.reload() + elif (node.name == '3DC_alpha'): + node.image.reload() + + +def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #read textures from texture file + + # Let's check are we UVSet or MATERIAL modee + create_nodes = False + for ind, index_mat in enumerate(objekti.material_slots): + + if(udim_textures): + tile_list = UVTiling(objekti,ind, texturelist) + else: + tile_list = [] + + texcoat = {} + texcoat['color'] = [] + texcoat['ao'] = [] + texcoat['rough'] = [] + texcoat['metalness'] = [] + texcoat['nmap'] = [] + texcoat['emissive'] = [] + texcoat['emissive_power'] = [] + texcoat['displacement'] = [] + texcoat['alpha'] = [] + + create_group_node = False + if(udim_textures == False): + for slot_index, texture_info in enumerate(texturelist): + + uv_MODE_mat = 'MAT' + for index, layer in enumerate(objekti.data.uv_layers): + if(layer.name == texturelist[slot_index][0]): + uv_MODE_mat = 'UV' + break + + print('aaa') + print(texture_info[0]) + print(index_mat) + if(texture_info[0] == index_mat.name): + if texture_info[2] == 'color' or texture_info[2] == 'diffuse': + if(index_mat.material.coat3D_diffuse): + + texcoat['color'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2] == 'metalness' or texture_info[2] == 'specular' or texture_info[2] == 'reflection': + if (index_mat.material.coat3D_metalness): + texcoat['metalness'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2] == 'rough' or texture_info[2] == 'roughness': + if (index_mat.material.coat3D_roughness): + texcoat['rough'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2] == 'nmap' or texture_info[2] == 'normalmap' or texture_info[2] == 'normal_map' or texture_info[2] == 'normal': + if (index_mat.material.coat3D_normal): + texcoat['nmap'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2] == 'emissive': + if (index_mat.material.coat3D_emissive): + texcoat['emissive'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2] == 'emissive_power': + if (index_mat.material.coat3D_emissive): + texcoat['emissive_power'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2] == 'ao': + if (index_mat.material.coat3D_ao): + texcoat['ao'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2].startswith('displacement'): + if (index_mat.material.coat3D_displacement): + texcoat['displacement'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + elif texture_info[2] == 'alpha' or texture_info[2] == 'opacity': + if (index_mat.material.coat3D_alpha): + texcoat['alpha'].append(texture_info[3]) + create_nodes = True + else: + os.remove(texture_info[3]) + + create_group_node = True + + else: + for texture_info in texturelist: + + if texture_info[2] == 'color' or texture_info[2] == 'diffuse': + texcoat['color'].append([texture_info[0],texture_info[3]]) + create_nodes = True + elif texture_info[2] == 'metalness' or texture_info[2] == 'specular' or texture_info[ + 2] == 'reflection': + texcoat['metalness'].append([texture_info[0],texture_info[3]]) + create_nodes = True + elif texture_info[2] == 'rough' or texture_info[2] == 'roughness': + texcoat['rough'].append([texture_info[0],texture_info[3]]) + create_nodes = True + elif texture_info[2] == 'nmap' or texture_info[2] == 'normalmap' or texture_info[ + 2] == 'normal_map' or texture_info[2] == 'normal': + texcoat['nmap'].append([texture_info[0],texture_info[3]]) + create_nodes = True + elif texture_info[2] == 'emissive': + texcoat['emissive'].append([texture_info[0],texture_info[3]]) + create_nodes = True + elif texture_info[2] == 'emissive_power': + texcoat['emissive_power'].append([texture_info[0],texture_info[3]]) + create_nodes = True + elif texture_info[2] == 'ao': + texcoat['ao'].append([texture_info[0],texture_info[3]]) + create_nodes = True + elif texture_info[2].startswith('displacement'): + texcoat['displacement'].append([texture_info[0],texture_info[3]]) + create_nodes = True + if texture_info[2] == 'alpha' or texture_info[2] == 'opacity': + texcoat['alpha'].append([texture_info[0], texture_info[3]]) + create_nodes = True + create_group_node = True + + if(create_nodes): + coat3D = bpy.context.scene.coat3D + path3b_n = coat3D.exchangedir + path3b_n += ('%slast_saved_3b_file.txt' % (os.sep)) + + if (os.path.isfile(path3b_n)): + export_file = open(path3b_n) + for line in export_file: + objekti.coat3D.applink_3b_path = line + export_file.close() + coat3D.remove_path = True + createnodes(index_mat, texcoat, create_group_node, tile_list, objekti, ind, is_new) + +def createnodes(active_mat,texcoat, create_group_node, tile_list, objekti, ind, is_new): # Creates new nodes and link textures into them + bring_color = True # Meaning of these is to check if we can only update textures or do we need to create new nodes + bring_metalness = True + bring_roughness = True + bring_normal = True + bring_displacement = True + bring_emissive = True + bring_AO = True + bring_alpha = True + + coat3D = bpy.context.scene.coat3D + coatMat = active_mat.material + + if(coatMat.use_nodes == False): + coatMat.use_nodes = True + + act_material = coatMat.node_tree + main_material = coatMat.node_tree + applink_group_node = False + + # First go through all image nodes and let's check if it starts with 3DC and reload if needed + + for node in coatMat.node_tree.nodes: + if (node.type == 'OUTPUT_MATERIAL'): + out_mat = node + break + + for node in act_material.nodes: + if(node.name == '3DC_Applink' and node.type == 'GROUP'): + applink_group_node = True + act_material = node.node_tree + applink_tree = node + break + + for node in act_material.nodes: + if (node.type != 'GROUP'): + if (node.type != 'GROUP_OUTPUT'): + if (node.type == 'TEX_IMAGE'): + if (node.name == '3DC_color'): + bring_color = False + elif (node.name == '3DC_metalness'): + bring_metalness = False + elif (node.name == '3DC_rough'): + bring_roughness = False + elif (node.name == '3DC_nmap'): + bring_normal = False + elif (node.name == '3DC_displacement'): + bring_displacement = False + elif (node.name == '3DC_emissive'): + bring_emissive = False + elif (node.name == '3DC_AO'): + bring_AO = False + elif (node.name == '3DC_alpha'): + bring_alpha = False + elif (node.type == 'GROUP' and node.name.startswith('3DC_')): + if (node.name == '3DC_color'): + bring_color = False + elif (node.name == '3DC_metalness'): + bring_metalness = False + elif (node.name == '3DC_rough'): + bring_roughness = False + elif (node.name == '3DC_nmap'): + bring_normal = False + elif (node.name == '3DC_displacement'): + bring_displacement = False + elif (node.name == '3DC_emissive'): + bring_emissive = False + elif (node.name == '3DC_AO'): + bring_AO = False + elif (node.name == '3DC_alpha'): + bring_alpha = False + + #Let's start to build new node tree. Let's start linking with Material Output + + if(create_group_node): + if(applink_group_node == False): + main_mat2 = out_mat.inputs['Surface'].links[0].from_node + for input_ind in main_mat2.inputs: + if(input_ind.is_linked): + main_mat3 = input_ind.links[0].from_node + if(main_mat3.type == 'BSDF_PRINCIPLED'): + main_mat = main_mat3 + + group_tree = bpy.data.node_groups.new( type="ShaderNodeTree", name="3DC_Applink") + group_tree.outputs.new("NodeSocketColor", "Color") + group_tree.outputs.new("NodeSocketColor", "Metallic") + group_tree.outputs.new("NodeSocketColor", "Roughness") + group_tree.outputs.new("NodeSocketVector", "Normal map") + group_tree.outputs.new("NodeSocketColor", "Emissive") + group_tree.outputs.new("NodeSocketColor", "Displacement") + group_tree.outputs.new("NodeSocketColor", "Emissive Power") + group_tree.outputs.new("NodeSocketColor", "AO") + group_tree.outputs.new("NodeSocketColor", "Alpha") + applink_tree = act_material.nodes.new('ShaderNodeGroup') + applink_tree.name = '3DC_Applink' + applink_tree.node_tree = group_tree + applink_tree.location = -400, -100 + act_material = group_tree + notegroup = act_material.nodes.new('NodeGroupOutput') + notegroup.location = 220, -260 + + if(texcoat['emissive'] != []): + from_output = out_mat.inputs['Surface'].links[0].from_node + if(from_output.type == 'BSDF_PRINCIPLED'): + add_shader = main_material.nodes.new('ShaderNodeAddShader') + emission_shader = main_material.nodes.new('ShaderNodeEmission') + + emission_shader.name = '3DC_Emission' + + add_shader.location = 420, 110 + emission_shader.location = 70, -330 + out_mat.location = 670, 130 + + main_material.links.new(from_output.outputs[0], add_shader.inputs[0]) + main_material.links.new(add_shader.outputs[0], out_mat.inputs[0]) + main_material.links.new(emission_shader.outputs[0], add_shader.inputs[1]) + main_mat = from_output + else: + main_mat = out_mat.inputs['Surface'].links[0].from_node + + else: + main_mat = out_mat.inputs['Surface'].links[0].from_node + index = 0 + for node in coatMat.node_tree.nodes: + if (node.type == 'GROUP' and node.name =='3DC_Applink'): + for in_node in node.node_tree.nodes: + if(in_node.type == 'GROUP_OUTPUT'): + notegroup = in_node + index = 1 + break + if(index == 1): + break + + # READ DATA.JSON FILE + + json_address = os.path.dirname(bpy.app.binary_path) + os.sep + str(bpy.app.version[0]) + '.' + str(bpy.app.version[1]) + os.sep + 'scripts' + os.sep + 'addons' + os.sep + 'io_coat3D' + os.sep + 'data.json' + with open(json_address, encoding='utf-8') as data_file: + data = json.loads(data_file.read()) + + if(out_mat.inputs['Surface'].is_linked == True): + if(bring_color == True and texcoat['color'] != []): + CreateTextureLine(data['color'], act_material, main_mat, texcoat, coat3D, notegroup, + main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + + if(bring_metalness == True and texcoat['metalness'] != []): + CreateTextureLine(data['metalness'], act_material, main_mat, texcoat, coat3D, notegroup, + main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + + if(bring_roughness == True and texcoat['rough'] != []): + CreateTextureLine(data['rough'], act_material, main_mat, texcoat, coat3D, notegroup, + main_material, applink_tree, out_mat, coatMat,tile_list, objekti, ind, is_new) + + if(bring_normal == True and texcoat['nmap'] != []): + CreateTextureLine(data['nmap'], act_material, main_mat, texcoat, coat3D, notegroup, + main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + + if (bring_emissive == True and texcoat['emissive'] != []): + CreateTextureLine(data['emissive'], act_material, main_mat, texcoat, coat3D, notegroup, + main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + + if (bring_displacement == True and texcoat['displacement'] != []): + CreateTextureLine(data['displacement'], act_material, main_mat, texcoat, coat3D, notegroup, + main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + if (bring_alpha == True and texcoat['alpha'] != []): + CreateTextureLine(data['alpha'], act_material, main_mat, texcoat, coat3D, notegroup, + main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new) + + +def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, main_material, applink_tree, out_mat, coatMat, tile_list, objekti, ind, is_new): + + if(tile_list): + texture_name = coatMat.name + '_' + type['name'] + texture_tree = bpy.data.node_groups.new(type="ShaderNodeTree", name=texture_name) + texture_tree.outputs.new("NodeSocketColor", "Color") + texture_tree.outputs.new("NodeSocketColor", "Alpha") + texture_node_tree = act_material.nodes.new('ShaderNodeGroup') + texture_node_tree.name = '3DC_' + type['name'] + texture_node_tree.node_tree = texture_tree + texture_node_tree.location[0] = type['node_location'][0] + texture_node_tree.location[0] -= 400 + texture_node_tree.location[1] = type['node_location'][1] + notegroupend = texture_tree.nodes.new('NodeGroupOutput') + + count = len(tile_list) + uv_loc = [-1400, 200] + map_loc = [-1100, 200] + tex_loc = [-700, 200] + mix_loc = [-400, 100] + + nodes = [] + + for index, tile in enumerate(tile_list): + + tex_img_node = texture_tree.nodes.new('ShaderNodeTexImage') + + for ind, tex_index in enumerate(texcoat[type['name']]): + if(tex_index[0] == tile): + tex_img_node.image = bpy.data.images.load(texcoat[type['name']][ind][1]) + break + tex_img_node.location = tex_loc + + if tex_img_node.image and type['colorspace'] != 'color': + tex_img_node.image.colorspace_settings.is_data = True + + tex_uv_node = texture_tree.nodes.new('ShaderNodeUVMap') + tex_uv_node.location = uv_loc + if(is_new): + tex_uv_node.uv_map = objekti.data.uv_layers[ind].name + else: + tex_uv_node.uv_map = objekti.data.uv_layers[0].name + + map_node = texture_tree.nodes.new('ShaderNodeMapping') + map_node.location = map_loc + map_node.name = '3DC_' + tile + map_node.vector_type = 'TEXTURE' + + tile_int_x = int(tile[3]) + tile_int_y = int(tile[2]) + + min_node = texture_tree.nodes.new('ShaderNodeVectorMath') + min_node.operation = "MINIMUM" + min_node.inputs[1].default_value[0] = tile_int_x - 1 + min_node.inputs[1].default_value[1] = tile_int_y + + max_node = texture_tree.nodes.new('ShaderNodeVectorMath') + max_node.operation = "MAXIMUM" + max_node.inputs[1].default_value[0] = tile_int_x + max_node.inputs[1].default_value[1] = tile_int_y + 1 + + + if(index == 0): + nodes.append(tex_img_node.name) + if(count == 1): + texture_tree.links.new(tex_img_node.outputs[0], notegroupend.inputs[0]) + texture_tree.links.new(tex_img_node.outputs[1], notegroupend.inputs[1]) + + if(index == 1): + mix_node = texture_tree.nodes.new('ShaderNodeMixRGB') + mix_node.blend_type = 'ADD' + mix_node.inputs[0].default_value = 1 + mix_node.location = mix_loc + mix_loc[1] -= 300 + texture_tree.links.new(tex_img_node.outputs[0], mix_node.inputs[2]) + texture_tree.links.new(texture_tree.nodes[nodes[0]].outputs[0], mix_node.inputs[1]) + mix_node_alpha = texture_tree.nodes.new('ShaderNodeMath') + mix_node_alpha.location = mix_loc + mix_loc[1] -= 200 + texture_tree.links.new(tex_img_node.outputs[1], mix_node_alpha.inputs[1]) + texture_tree.links.new(texture_tree.nodes[nodes[0]].outputs[1], mix_node_alpha.inputs[0]) + nodes.clear() + nodes.append(tex_img_node.name) + nodes.append(mix_node.name) + nodes.append(mix_node_alpha.name) + + + elif(index > 1): + mix_node = texture_tree.nodes.new('ShaderNodeMixRGB') + mix_node.blend_type = 'ADD' + mix_node.inputs[0].default_value = 1 + mix_node.location = mix_loc + mix_loc[1] -= 300 + texture_tree.links.new(texture_tree.nodes[nodes[1]].outputs[0], mix_node.inputs[1]) + texture_tree.links.new(tex_img_node.outputs[0], mix_node.inputs[2]) + mix_node_alpha = texture_tree.nodes.new('ShaderNodeMath') + mix_node_alpha.location = mix_loc + mix_loc[1] -= 200 + texture_tree.links.new(texture_tree.nodes[nodes[2]].outputs[0], mix_node_alpha.inputs[0]) + texture_tree.links.new(tex_img_node.outputs[1], mix_node_alpha.inputs[1]) + + nodes.clear() + nodes.append(tex_img_node.name) + nodes.append(mix_node.name) + nodes.append(mix_node_alpha.name) + + tex_loc[1] -= 300 + uv_loc[1] -= 300 + map_loc[1] -= 300 + + texture_tree.links.new(tex_uv_node.outputs[0], map_node.inputs[0]) + texture_tree.links.new(map_node.outputs[0], min_node.inputs[0]) + texture_tree.links.new(min_node.outputs['Vector'], max_node.inputs[0]) + texture_tree.links.new(max_node.outputs['Vector'], tex_img_node.inputs[0]) + + if(count > 1): + texture_tree.links.new(mix_node.outputs[0], notegroupend.inputs[0]) + texture_tree.links.new(mix_node_alpha.outputs[0], notegroupend.inputs[1]) + + if(tile_list): + node = texture_node_tree + if(texcoat['alpha'] != []): + if (type['name'] == 'color'): + act_material.links.new(node.outputs[1], notegroup.inputs[8]) + else: + if(type['name'] == 'alpha'): + act_material.links.new(node.outputs[1], notegroup.inputs[8]) + + + else: + node = act_material.nodes.new('ShaderNodeTexImage') + uv_node = act_material.nodes.new('ShaderNodeUVMap') + + uv_node.uv_map = objekti.data.uv_layers[0].name + act_material.links.new(uv_node.outputs[0], node.inputs[0]) + uv_node.use_custom_color = True + uv_node.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + + node.use_custom_color = True + node.color = (type['node_color'][0],type['node_color'][1],type['node_color'][2]) + + + if type['name'] == 'nmap': + normal_node = act_material.nodes.new('ShaderNodeNormalMap') + normal_node.use_custom_color = True + normal_node.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + + node.location = -671, -510 + if(tile_list == []): + uv_node.location = -750, -600 + normal_node.location = -350, -350 + normal_node.name = '3DC_normalnode' + + elif type['name'] == 'displacement': + disp_node = main_material.nodes.new('ShaderNodeDisplacement') + + node.location = -630, -1160 + disp_node.location = 90, -460 + disp_node.inputs[2].default_value = 0.1 + disp_node.name = '3DC_dispnode' + + node.name = '3DC_' + type['name'] + node.label = type['name'] + + if (type['name'] != 'displacement'): + for input_index in type['find_input']: + input_color = main_mat.inputs.find(input_index) + if(input_color != -1): + break + + if (tile_list == []): + + load_image = True + + for image in bpy.data.images: + if(texcoat[type['name']][0] == image.filepath): + load_image = False + node.image = image + break + + if (load_image): + node.image = bpy.data.images.load(texcoat[type['name']][0]) + + if node.image and type['colorspace'] == 'noncolor': + node.image.colorspace_settings.is_data = True + + if (coat3D.createnodes): + + if(type['name'] == 'nmap'): + act_material.links.new(node.outputs[0], normal_node.inputs[1]) + if(input_color != -1): + act_material.links.new(normal_node.outputs[0], main_mat.inputs[input_color]) + + act_material.links.new(normal_node.outputs[0], notegroup.inputs[type['input']]) + if (main_mat.inputs[input_color].name == 'Normal' and input_color != -1): + main_material.links.new(applink_tree.outputs[type['input']], main_mat.inputs[input_color]) + + elif (type['name'] == 'displacement'): + + rampnode = act_material.nodes.new('ShaderNodeValToRGB') + rampnode.name = '3DC_ColorRamp' + rampnode.use_custom_color = True + rampnode.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + rampnode.location = -270, -956 + + act_material.links.new(node.outputs[0], rampnode.inputs[0]) + act_material.links.new(rampnode.outputs[0], notegroup.inputs[5]) + + main_material.links.new(applink_tree.outputs[5], disp_node.inputs[0]) + main_material.links.new(disp_node.outputs[0], out_mat.inputs[2]) + coatMat.cycles.displacement_method = 'BOTH' + + else: + if (texcoat['alpha'] != []): + if (type['name'] == 'alpha'): + act_material.links.new(node.outputs[1], notegroup.inputs[8]) + else: + if (type['name'] == 'color'): + act_material.links.new(node.outputs[1], notegroup.inputs[8]) + if(type['name'] != 'alpha'): + huenode = createExtraNodes(act_material, node, type, notegroup) + else: + huenode = node + huenode.location = -100, -800 + + if(type['name'] != 'alpha'): + act_material.links.new(huenode.outputs[0], notegroup.inputs[type['input']]) + if (main_mat.type != 'MIX_SHADER' and input_color != -1): + main_material.links.new(applink_tree.outputs[type['input']], main_mat.inputs[input_color]) + if(type['name'] == 'color'): #Alpha connection into Principled shader + main_material.links.new(applink_tree.outputs['Alpha'], main_mat.inputs['Alpha']) + + else: + location = main_mat.location + #applink_tree.location = main_mat.location[0], main_mat.location[1] + 200 + + if(type['name'] == 'emissive'): + for material in main_material.nodes: + if(material.name == '3DC_Emission'): + main_material.links.new(applink_tree.outputs[type['input']], material.inputs[0]) + break + if(tile_list == []): + uv_node.location = node.location + uv_node.location[0] -= 300 + uv_node.location[1] -= 200 + + else: + node.location = type['node_location'][0], type['node_location'][1] + if (tile_list == []): + uv_node.location = node.location + uv_node.location[0] -= 300 + act_material.links.new(node.outputs[0], notegroup.inputs[type['input']]) + if (input_color != -1): + main_material.links.new(applink_tree.outputs[type['input']], main_mat.inputs[input_color]) + + +def createExtraNodes(act_material, node, type, notegroup): + + curvenode = act_material.nodes.new('ShaderNodeRGBCurve') + curvenode.name = '3DC_RGBCurve' + curvenode.use_custom_color = True + curvenode.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + + if(type['huenode'] == 'yes'): + huenode = act_material.nodes.new('ShaderNodeHueSaturation') + huenode.name = '3DC_HueSaturation' + huenode.use_custom_color = True + huenode.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + else: + huenode = act_material.nodes.new('ShaderNodeMath') + huenode.name = '3DC_HueSaturation' + huenode.operation = 'MULTIPLY' + huenode.inputs[1].default_value = 1 + huenode.use_custom_color = True + huenode.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + + + if(type['rampnode'] == 'yes'): + rampnode = act_material.nodes.new('ShaderNodeValToRGB') + rampnode.name = '3DC_ColorRamp' + rampnode.use_custom_color = True + rampnode.color = (type['node_color'][0], type['node_color'][1], type['node_color'][2]) + + if (type['rampnode'] == 'yes'): + act_material.links.new(node.outputs[0], curvenode.inputs[1]) + act_material.links.new(curvenode.outputs[0], rampnode.inputs[0]) + if(type['huenode'] == 'yes'): + act_material.links.new(rampnode.outputs[0], huenode.inputs[4]) + else: + act_material.links.new(rampnode.outputs[0], huenode.inputs[0]) + else: + act_material.links.new(node.outputs[0], curvenode.inputs[1]) + if (type['huenode'] == 'yes'): + act_material.links.new(curvenode.outputs[0], huenode.inputs[4]) + else: + act_material.links.new(curvenode.outputs[0], huenode.inputs[0]) + + if type['name'] == 'metalness': + node.location = -1300, 119 + curvenode.location = -1000, 113 + rampnode.location = -670, 115 + huenode.location = -345, 118 + + elif type['name'] == 'rough': + node.location = -1300, -276 + curvenode.location = -1000, -245 + rampnode.location = -670, -200 + huenode.location = -340, -100 + + elif type['name'] == 'color': + node.location = -990, 530 + curvenode.location = -660, 480 + huenode.location = -337, 335 + + elif type['name'] == 'emissive': + node.location = -1200, -900 + curvenode.location = -900, -900 + huenode.location = -340, -700 + + elif type['name'] == 'alpha': + node.location = -1200, -1200 + curvenode.location = -900, -1250 + rampnode.location = -600, -1200 + huenode.location = -300, -1200 + + if(type['name'] == 'color'): + node_vertex = act_material.nodes.new('ShaderNodeVertexColor') + node_mixRGB = act_material.nodes.new('ShaderNodeMixRGB') + node_vectormath = act_material.nodes.new('ShaderNodeVectorMath') + + node_mixRGB.blend_type = 'MULTIPLY' + node_mixRGB.inputs[0].default_value = 1 + + node_vectormath.operation = 'MULTIPLY' + node_vectormath.inputs[1].default_value = [2,2,2] + + node_vertex.layer_name = 'Col' + + node_vertex.location = -337, 525 + node_mixRGB.location = 0, 425 + + act_material.links.new(node_vertex.outputs[0], node_mixRGB.inputs[1]) + act_material.links.new(huenode.outputs[0], node_mixRGB.inputs[2]) + act_material.links.new(node_vertex.outputs[1], notegroup.inputs[8]) + act_material.links.new(node_mixRGB.outputs[0], node_vectormath.inputs[0]) + + return node_vectormath + + return huenode + +def matlab(objekti,mat_list,texturelist,is_new): + + print('Welcome facture matlab function') + + ''' FBX Materials: remove all nodes and create princibles node''' + if(is_new): + RemoveFbxNodes(objekti) + + '''Main Loop for Texture Update''' + + updatetextures(objekti) + + ''' Check if bind textures with UVs or Materials ''' + + if(texturelist != []): + + udim_textures = False + if texturelist[0][0].startswith('100'): + udim_textures = True + + if(udim_textures == False): + readtexturefolder(objekti,mat_list,texturelist,is_new, udim_textures) + else: + path = texturelist[0][3] + only_name = os.path.basename(path) + if(only_name.startswith(objekti.coat3D.applink_index)): + readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures) + + + return('FINISHED') -- cgit v1.2.3 From 52edc5f41f231eac04415f3722a354ab5972b03a Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Sat, 18 Jul 2020 00:32:07 -0400 Subject: Collection Manager: Add to menus. Task: T69577 Add the main collection manager window and the QCD move widget to the Object->Collection menu, formerly these were only accessible through hotkeys. Improve tooltips to better describe what these do. --- object_collection_manager/__init__.py | 20 +++++++++++++++++++- object_collection_manager/qcd_move_widget.py | 2 +- object_collection_manager/ui.py | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 928a62ec..e8b8f25c 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 9, 2), + "version": (2, 9, 3), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -133,6 +133,18 @@ def undo_redo_post_handler(dummy): internals.move_selection.clear() internals.move_active = None + +def menu_addition(self, context): + layout = self.layout + + layout.operator('view3d.collection_manager') + + if bpy.context.preferences.addons[__package__].preferences.enable_qcd: + layout.operator('view3d.qcd_move_widget') + + layout.separator() + + def register(): for cls in classes: bpy.utils.register_class(cls) @@ -145,6 +157,9 @@ def register(): kmi = km.keymap_items.new('view3d.collection_manager', 'M', 'PRESS') addon_keymaps.append((km, kmi)) + # Add Collection Manager & QCD Move Widget to the Object->Collections menu + bpy.types.VIEW3D_MT_object_collection.prepend(menu_addition) + bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post_handler) bpy.app.handlers.undo_post.append(undo_redo_post_handler) bpy.app.handlers.redo_post.append(undo_redo_post_handler) @@ -159,6 +174,9 @@ def unregister(): for cls in classes: bpy.utils.unregister_class(cls) + # Remove Collection Manager & QCD Move Widget from the Object->Collections menu + bpy.types.VIEW3D_MT_object_collection.remove(menu_addition) + bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post_handler) bpy.app.handlers.undo_post.remove(undo_redo_post_handler) bpy.app.handlers.redo_post.remove(undo_redo_post_handler) diff --git a/object_collection_manager/qcd_move_widget.py b/object_collection_manager/qcd_move_widget.py index 95e25058..85f63f58 100644 --- a/object_collection_manager/qcd_move_widget.py +++ b/object_collection_manager/qcd_move_widget.py @@ -372,7 +372,7 @@ def update_area_dimensions(area, w=0, h=0): area["height"] += h class QCDMoveWidget(Operator): - """QCD Move Widget""" + """Move objects to QCD Slots""" bl_idname = "view3d.qcd_move_widget" bl_label = "QCD Move Widget" diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 3bd614a6..76c44bb0 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -58,6 +58,7 @@ last_icon_theme_text_sel = None class CollectionManager(Operator): + '''Manage and control collections, with advanced features, in a popup UI''' bl_label = "Collection Manager" bl_idname = "view3d.collection_manager" -- cgit v1.2.3 From 0657e99e1f4ff01118591d19ffc740694869ba96 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Sat, 18 Jul 2020 00:48:13 -0400 Subject: Collection Manager: Fix remove issue. Task: T69577 Fix error when removing a collection with a child that is already linked to the parent collection. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/operators.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index e8b8f25c..1b33c549 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 9, 3), + "version": (2, 9, 4), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 54f9596b..77882c97 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -897,7 +897,8 @@ class CMRemoveCollectionOperator(Operator): # link any subcollections of the to be deleted collection to it's parent for subcollection in collection.children: - parent_collection.children.link(subcollection) + if not subcollection.name in parent_collection.children: + parent_collection.children.link(subcollection) # apply the stored view layer RTOs to the newly linked collections and their # children -- cgit v1.2.3 From 099e4eeb7b1cc9c41e35321e9c076c51c4cd2e6e Mon Sep 17 00:00:00 2001 From: Julian Eisel Date: Mon, 20 Jul 2020 20:45:04 +0200 Subject: VR Scene Inspection: Show note if built without WITH_XR_OPENXR Otherwise the Add-on causes no visible changes, which can be confusing. Especially since the flag is disabled by default on macOS. --- viewport_vr_preview.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/viewport_vr_preview.py b/viewport_vr_preview.py index 8dc865f5..db8dd176 100644 --- a/viewport_vr_preview.py +++ b/viewport_vr_preview.py @@ -313,6 +313,20 @@ class VIEW3D_PT_vr_session(bpy.types.Panel): layout.prop(session_settings, "use_positional_tracking") +class VIEW3D_PT_vr_info(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "VR" + bl_label = "VR Info" + + @classmethod + def poll(cls, context): + return not bpy.app.build_options.xr_openxr + + def draw(self, context): + layout = self.layout + layout.label(icon='ERROR', text="Built without VR/OpenXR features.") + class VIEW3D_OT_vr_landmark_add(bpy.types.Operator): bl_idname = "view3d.vr_landmark_add" bl_label = "Add VR Landmark" @@ -503,6 +517,7 @@ classes = ( def register(): if not bpy.app.build_options.xr_openxr: + bpy.utils.register_class(VIEW3D_PT_vr_info) return for cls in classes: @@ -529,6 +544,7 @@ def register(): def unregister(): if not bpy.app.build_options.xr_openxr: + bpy.utils.unregister_class(VIEW3D_PT_vr_info) return for cls in classes: -- cgit v1.2.3 From 4eb733d819f2c3f873202ba645e411526ffc469f Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 21 Jul 2020 00:33:05 -0400 Subject: Collection Manager: Cleanup. Task: T69577 Standardize setting operator properties. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/ui.py | 37 +++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 1b33c549..f6a31695 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 9, 4), + "version": (2, 9, 5), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 76c44bb0..6d576029 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -305,11 +305,13 @@ class CollectionManager(Operator): # add collections addcollec_row = layout.row() - addcollec_row.operator("view3d.add_collection", text="Add Collection", - icon='COLLECTION_NEW').child = False + prop = addcollec_row.operator("view3d.add_collection", text="Add Collection", + icon='COLLECTION_NEW') + prop.child = False addcollec_row.operator("view3d.add_collection", text="Add SubCollection", - icon='COLLECTION_NEW').child = True + icon='COLLECTION_NEW') + prop.child = True # phantom mode phantom_row = layout.row() @@ -558,8 +560,9 @@ class CM_UL_items(UIList): highlight = bool(exclude_history and exclude_target == item.name) icon = 'CHECKBOX_DEHLT' if laycol["ptr"].exclude else 'CHECKBOX_HLT' - row.operator("view3d.exclude_collection", text="", icon=icon, - emboss=highlight, depress=highlight).name = item.name + prop = row.operator("view3d.exclude_collection", text="", icon=icon, + emboss=highlight, depress=highlight) + prop.name = item.name if cm.show_selectable: select_history_base = rto_history["select"].get(view_layer.name, {}) @@ -570,8 +573,9 @@ class CM_UL_items(UIList): icon = ('RESTRICT_SELECT_ON' if laycol["ptr"].collection.hide_select else 'RESTRICT_SELECT_OFF') - row.operator("view3d.restrict_select_collection", text="", icon=icon, - emboss=highlight, depress=highlight).name = item.name + prop = row.operator("view3d.restrict_select_collection", text="", icon=icon, + emboss=highlight, depress=highlight) + prop.name = item.name if cm.show_hide_viewport: hide_history_base = rto_history["hide"].get(view_layer.name, {}) @@ -581,8 +585,9 @@ class CM_UL_items(UIList): highlight = bool(hide_history and hide_target == item.name) icon = 'HIDE_ON' if laycol["ptr"].hide_viewport else 'HIDE_OFF' - row.operator("view3d.hide_collection", text="", icon=icon, - emboss=highlight, depress=highlight).name = item.name + prop = row.operator("view3d.hide_collection", text="", icon=icon, + emboss=highlight, depress=highlight) + prop.name = item.name if cm.show_disable_viewport: disable_history_base = rto_history["disable"].get(view_layer.name, {}) @@ -593,8 +598,9 @@ class CM_UL_items(UIList): icon = ('RESTRICT_VIEW_ON' if laycol["ptr"].collection.hide_viewport else 'RESTRICT_VIEW_OFF') - row.operator("view3d.disable_viewport_collection", text="", icon=icon, - emboss=highlight, depress=highlight).name = item.name + prop = row.operator("view3d.disable_viewport_collection", text="", icon=icon, + emboss=highlight, depress=highlight) + prop.name = item.name if cm.show_render: render_history_base = rto_history["render"].get(view_layer.name, {}) @@ -605,8 +611,9 @@ class CM_UL_items(UIList): icon = ('RESTRICT_RENDER_ON' if laycol["ptr"].collection.hide_render else 'RESTRICT_RENDER_OFF') - row.operator("view3d.disable_render_collection", text="", icon=icon, - emboss=highlight, depress=highlight).name = item.name + prop = row.operator("view3d.disable_render_collection", text="", icon=icon, + emboss=highlight, depress=highlight) + prop.name = item.name @@ -616,8 +623,8 @@ class CM_UL_items(UIList): row.separator() rm_op = row.row() - rm_op.operator("view3d.remove_collection", text="", icon='X', - emboss=False).collection_name = item.name + prop = rm_op.operator("view3d.remove_collection", text="", icon='X', emboss=False) + prop.collection_name = item.name if len(data.cm_list_collection) > index + 1: -- cgit v1.2.3 From c5bee7deff9d275a9edb6abff261e2b5506efd48 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 21 Jul 2020 00:41:47 -0400 Subject: Collection Manager: Fix cleanup. Task: T69577 --- object_collection_manager/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 6d576029..838c5d18 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -309,7 +309,7 @@ class CollectionManager(Operator): icon='COLLECTION_NEW') prop.child = False - addcollec_row.operator("view3d.add_collection", text="Add SubCollection", + prop = addcollec_row.operator("view3d.add_collection", text="Add SubCollection", icon='COLLECTION_NEW') prop.child = True -- cgit v1.2.3 From 40db41a902be5dfd8305ea389aa5e8eec1aa74d6 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:11:07 +0200 Subject: glTF exporter: Allow to call a pre and post callbacks Thanks jjcasmar! --- io_scene_gltf2/__init__.py | 10 +++++++++- io_scene_gltf2/blender/exp/gltf2_blender_export.py | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 211839f8..cb663fca 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 30), + "version": (1, 3, 31), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -485,6 +485,8 @@ class ExportGLTF2_Base: bpy.path.ensure_ext(self.filepath,self.filename_ext)))[0] + '.bin' user_extensions = [] + pre_export_callbacks = [] + post_export_callbacks = [] import sys preferences = bpy.context.preferences @@ -500,7 +502,13 @@ class ExportGLTF2_Base: extension_ctors = module.glTF2ExportUserExtensions for extension_ctor in extension_ctors: user_extensions.append(extension_ctor()) + if hasattr(module, 'glTF2_pre_export_callback'): + pre_export_callbacks.append(module.glTF2_pre_export_callback) + if hasattr(module, 'glTF2_post_export_callback'): + post_export_callbacks.append(module.glTF2_post_export_callback) export_settings['gltf_user_extensions'] = user_extensions + export_settings['pre_export_callbacks'] = pre_export_callbacks + export_settings['post_export_callbacks'] = post_export_callbacks return gltf2_blender_export.save(context, export_settings) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py index 2989ec31..fd433c7e 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_export.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py @@ -39,7 +39,15 @@ def save(context, export_settings): __notify_start(context) start_time = time.time() + pre_export_callbacks = export_settings["pre_export_callbacks"] + for callback in pre_export_callbacks: + callback(export_settings) + json, buffer = __export(export_settings) + + post_export_callbacks = export_settings["post_export_callbacks"] + for callback in post_export_callbacks: + callback(export_settings) __write_file(json, buffer, export_settings) end_time = time.time() -- cgit v1.2.3 From 63dd8498ac106b5645822a124aa0edb0d917d5a8 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:14:06 +0200 Subject: glTF exporter: Add support for use_inherit_rotation and inherit_scale. Thanks Skywolf285! --- io_scene_gltf2/__init__.py | 2 +- .../exp/gltf2_blender_gather_animation_sampler_keyframes.py | 10 ++++++++-- io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py | 9 ++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index cb663fca..9329c556 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 31), + "version": (1, 3, 32), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py index 822aa6a1..f8ab333e 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py @@ -161,8 +161,14 @@ def get_bone_matrix(blender_object_if_armature: typing.Optional[bpy.types.Object if bake_bone is None: matrix = pbone.matrix_basis.copy() else: - matrix = pbone.matrix - matrix = blender_object_if_armature.convert_space(pose_bone=pbone, matrix=matrix, from_space='POSE', to_space='LOCAL') + if (pbone.bone.use_inherit_rotation == False or pbone.bone.inherit_scale != "FULL") and pbone.parent != None: + rest_mat = (pbone.parent.bone.matrix_local.inverted_safe() @ pbone.bone.matrix_local) + matrix = (rest_mat.inverted_safe() @ pbone.parent.matrix.inverted_safe() @ pbone.matrix) + else: + matrix = pbone.matrix + matrix = blender_object_if_armature.convert_space(pose_bone=pbone, matrix=matrix, from_space='POSE', to_space='LOCAL') + + data[frame][pbone.name] = matrix diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py index af086c1b..1cb26551 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py @@ -43,7 +43,14 @@ def gather_joint(blender_object, blender_bone, export_settings): else: correction_matrix_local = gltf2_blender_math.multiply( blender_bone.parent.bone.matrix_local.inverted(), blender_bone.bone.matrix_local) - matrix_basis = blender_bone.matrix_basis + + if (blender_bone.bone.use_inherit_rotation == False or blender_bone.bone.inherit_scale != "FULL") and blender_bone.parent != None: + rest_mat = (blender_bone.parent.bone.matrix_local.inverted_safe() @ blender_bone.bone.matrix_local) + matrix_basis = (rest_mat.inverted_safe() @ blender_bone.parent.matrix.inverted_safe() @ blender_bone.matrix) + else: + matrix_basis = blender_bone.matrix + matrix_basis = blender_object.convert_space(pose_bone=blender_bone, matrix=matrix_basis, from_space='POSE', to_space='LOCAL') + trans, rot, sca = gltf2_blender_extract.decompose_transition( gltf2_blender_math.multiply(correction_matrix_local, matrix_basis), export_settings) translation, rotation, scale = (None, None, None) -- cgit v1.2.3 From 2b4bf943d0a323e6b0430179cee7fa7ffb5907d3 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:16:08 +0200 Subject: glTF exporter: Fix exporting `aspectRatio` for Perspective Cameras Thanks pop! --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 9329c556..16fc681e 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 32), + "version": (1, 3, 33), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py index 585f0be3..bb211fe2 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py @@ -92,18 +92,18 @@ def __gather_perspective(blender_camera, export_settings): width = bpy.context.scene.render.pixel_aspect_x * bpy.context.scene.render.resolution_x height = bpy.context.scene.render.pixel_aspect_y * bpy.context.scene.render.resolution_y - perspective.aspectRatio = width / height + perspective.aspect_ratio = width / height if width >= height: if blender_camera.sensor_fit != 'VERTICAL': - perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio) + perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspect_ratio) else: perspective.yfov = blender_camera.angle else: if blender_camera.sensor_fit != 'HORIZONTAL': perspective.yfov = blender_camera.angle else: - perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio) + perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspect_ratio) perspective.znear = blender_camera.clip_start perspective.zfar = blender_camera.clip_end -- cgit v1.2.3 From 3ea1673580ab68ecb713da3233a4f6beaafff5d9 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:19:01 +0200 Subject: glTF exporter: performance: using numpy Thanks scurest! --- io_scene_gltf2/__init__.py | 2 +- .../gltf2_blender_gather_primitive_attributes.py | 185 +++++++++------------ .../blender/exp/gltf2_blender_gather_primitives.py | 58 +------ io_scene_gltf2/blender/exp/gltf2_blender_utils.py | 67 -------- io_scene_gltf2/io/com/gltf2_io_constants.py | 12 ++ 5 files changed, 96 insertions(+), 228 deletions(-) delete mode 100755 io_scene_gltf2/blender/exp/gltf2_blender_utils.py diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 16fc681e..0beb10a1 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 33), + "version": (1, 3, 34), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py index f5856257..8912d921 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import numpy as np + from . import gltf2_blender_export_keys from io_scene_gltf2.io.com import gltf2_io from io_scene_gltf2.io.com import gltf2_io_constants from io_scene_gltf2.io.com import gltf2_io_debug from io_scene_gltf2.io.exp import gltf2_io_binary_data -from io_scene_gltf2.blender.exp import gltf2_blender_utils def gather_primitive_attributes(blender_primitive, export_settings): @@ -36,72 +37,79 @@ def gather_primitive_attributes(blender_primitive, export_settings): return attributes +def array_to_accessor(array, component_type, data_type, include_max_and_min=False): + dtype = gltf2_io_constants.ComponentType.to_numpy_dtype(component_type) + num_elems = gltf2_io_constants.DataType.num_elements(data_type) + + if type(array) is not np.ndarray: + array = np.array(array, dtype=dtype) + array = array.reshape(len(array) // num_elems, num_elems) + + assert array.dtype == dtype + assert array.shape[1] == num_elems + + amax = None + amin = None + if include_max_and_min: + amax = np.amax(array, axis=0).tolist() + amin = np.amin(array, axis=0).tolist() + + return gltf2_io.Accessor( + buffer_view=gltf2_io_binary_data.BinaryData(array.tobytes()), + byte_offset=None, + component_type=component_type, + count=len(array), + extensions=None, + extras=None, + max=amax, + min=amin, + name=None, + normalized=None, + sparse=None, + type=data_type, + ) + + def __gather_position(blender_primitive, export_settings): position = blender_primitive["attributes"]["POSITION"] - componentType = gltf2_io_constants.ComponentType.Float return { - "POSITION": gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData.from_list(position, componentType), - byte_offset=None, - component_type=componentType, - count=len(position) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3), - extensions=None, - extras=None, - max=gltf2_blender_utils.max_components(position, gltf2_io_constants.DataType.Vec3), - min=gltf2_blender_utils.min_components(position, gltf2_io_constants.DataType.Vec3), - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec3 + "POSITION": array_to_accessor( + position, + component_type=gltf2_io_constants.ComponentType.Float, + data_type=gltf2_io_constants.DataType.Vec3, + include_max_and_min=True ) } def __gather_normal(blender_primitive, export_settings): - if export_settings[gltf2_blender_export_keys.NORMALS]: - normal = blender_primitive["attributes"]['NORMAL'] - return { - "NORMAL": gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData.from_list(normal, gltf2_io_constants.ComponentType.Float), - byte_offset=None, - component_type=gltf2_io_constants.ComponentType.Float, - count=len(normal) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec3 - ) - } - return {} + if not export_settings[gltf2_blender_export_keys.NORMALS]: + return {} + normal = blender_primitive["attributes"].get('NORMAL') + if not normal: + return {} + return { + "NORMAL": array_to_accessor( + normal, + component_type=gltf2_io_constants.ComponentType.Float, + data_type=gltf2_io_constants.DataType.Vec3, + ) + } def __gather_tangent(blender_primitive, export_settings): - if export_settings[gltf2_blender_export_keys.TANGENTS]: - if blender_primitive["attributes"].get('TANGENT') is not None: - tangent = blender_primitive["attributes"]['TANGENT'] - return { - "TANGENT": gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData.from_list( - tangent, gltf2_io_constants.ComponentType.Float), - byte_offset=None, - component_type=gltf2_io_constants.ComponentType.Float, - count=len(tangent) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec4 - ) - } - - return {} + if not export_settings[gltf2_blender_export_keys.TANGENTS]: + return {} + tangent = blender_primitive["attributes"].get('TANGENT') + if not tangent: + return {} + return { + "TANGENT": array_to_accessor( + tangent, + component_type=gltf2_io_constants.ComponentType.Float, + data_type=gltf2_io_constants.DataType.Vec4, + ) + } def __gather_texcoord(blender_primitive, export_settings): @@ -111,20 +119,10 @@ def __gather_texcoord(blender_primitive, export_settings): tex_coord_id = 'TEXCOORD_' + str(tex_coord_index) while blender_primitive["attributes"].get(tex_coord_id) is not None: tex_coord = blender_primitive["attributes"][tex_coord_id] - attributes[tex_coord_id] = gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData.from_list( - tex_coord, gltf2_io_constants.ComponentType.Float), - byte_offset=None, + attributes[tex_coord_id] = array_to_accessor( + tex_coord, component_type=gltf2_io_constants.ComponentType.Float, - count=len(tex_coord) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec2), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec2 + data_type=gltf2_io_constants.DataType.Vec2, ) tex_coord_index += 1 tex_coord_id = 'TEXCOORD_' + str(tex_coord_index) @@ -138,20 +136,10 @@ def __gather_colors(blender_primitive, export_settings): color_id = 'COLOR_' + str(color_index) while blender_primitive["attributes"].get(color_id) is not None: internal_color = blender_primitive["attributes"][color_id] - attributes[color_id] = gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData.from_list( - internal_color, gltf2_io_constants.ComponentType.Float), - byte_offset=None, + attributes[color_id] = array_to_accessor( + internal_color, component_type=gltf2_io_constants.ComponentType.Float, - count=len(internal_color) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec4 + data_type=gltf2_io_constants.DataType.Vec4, ) color_index += 1 color_id = 'COLOR_' + str(color_index) @@ -173,20 +161,10 @@ def __gather_skins(blender_primitive, export_settings): # joints internal_joint = blender_primitive["attributes"][joint_id] - joint = gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData.from_list( - internal_joint, gltf2_io_constants.ComponentType.UnsignedShort), - byte_offset=None, + joint = array_to_accessor( + internal_joint, component_type=gltf2_io_constants.ComponentType.UnsignedShort, - count=len(internal_joint) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec4 + data_type=gltf2_io_constants.DataType.Vec4, ) attributes[joint_id] = joint @@ -201,21 +179,10 @@ def __gather_skins(blender_primitive, export_settings): factor = 1.0 / total internal_weight[idx:idx + 4] = [w * factor for w in weight_slice] - weight = gltf2_io.Accessor( - buffer_view=gltf2_io_binary_data.BinaryData.from_list( - internal_weight, gltf2_io_constants.ComponentType.Float), - byte_offset=None, + weight = array_to_accessor( + internal_weight, component_type=gltf2_io_constants.ComponentType.Float, - count=len(internal_weight) // gltf2_io_constants.DataType.num_elements( - gltf2_io_constants.DataType.Vec4), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec4 + data_type=gltf2_io_constants.DataType.Vec4, ) attributes[weight_id] = weight diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py index 22f0bc6d..1a2ae00d 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -21,7 +21,6 @@ from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.blender.exp import gltf2_blender_extract from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes -from io_scene_gltf2.blender.exp import gltf2_blender_utils from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials from io_scene_gltf2.io.com import gltf2_io @@ -160,26 +159,11 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings if blender_primitive["attributes"].get(target_position_id): target = {} internal_target_position = blender_primitive["attributes"][target_position_id] - binary_data = gltf2_io_binary_data.BinaryData.from_list( + target["POSITION"] = gltf2_blender_gather_primitive_attributes.array_to_accessor( internal_target_position, - gltf2_io_constants.ComponentType.Float - ) - target["POSITION"] = gltf2_io.Accessor( - buffer_view=binary_data, - byte_offset=None, component_type=gltf2_io_constants.ComponentType.Float, - count=len(internal_target_position) // gltf2_io_constants.DataType.num_elements( - gltf2_io_constants.DataType.Vec3), - extensions=None, - extras=None, - max=gltf2_blender_utils.max_components( - internal_target_position, gltf2_io_constants.DataType.Vec3), - min=gltf2_blender_utils.min_components( - internal_target_position, gltf2_io_constants.DataType.Vec3), - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec3 + data_type=gltf2_io_constants.DataType.Vec3, + include_max_and_min=True, ) if export_settings[NORMALS] \ @@ -187,48 +171,20 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings and blender_primitive["attributes"].get(target_normal_id): internal_target_normal = blender_primitive["attributes"][target_normal_id] - binary_data = gltf2_io_binary_data.BinaryData.from_list( + target['NORMAL'] = gltf2_blender_gather_primitive_attributes.array_to_accessor( internal_target_normal, - gltf2_io_constants.ComponentType.Float, - ) - target['NORMAL'] = gltf2_io.Accessor( - buffer_view=binary_data, - byte_offset=None, component_type=gltf2_io_constants.ComponentType.Float, - count=len(internal_target_normal) // gltf2_io_constants.DataType.num_elements( - gltf2_io_constants.DataType.Vec3), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec3 + data_type=gltf2_io_constants.DataType.Vec3, ) if export_settings[TANGENTS] \ and export_settings[MORPH_TANGENT] \ and blender_primitive["attributes"].get(target_tangent_id): internal_target_tangent = blender_primitive["attributes"][target_tangent_id] - binary_data = gltf2_io_binary_data.BinaryData.from_list( + target['TANGENT'] = gltf2_blender_gather_primitive_attributes.array_to_accessor( internal_target_tangent, - gltf2_io_constants.ComponentType.Float, - ) - target['TANGENT'] = gltf2_io.Accessor( - buffer_view=binary_data, - byte_offset=None, component_type=gltf2_io_constants.ComponentType.Float, - count=len(internal_target_tangent) // gltf2_io_constants.DataType.num_elements( - gltf2_io_constants.DataType.Vec3), - extensions=None, - extras=None, - max=None, - min=None, - name=None, - normalized=None, - sparse=None, - type=gltf2_io_constants.DataType.Vec3 + data_type=gltf2_io_constants.DataType.Vec3, ) targets.append(target) morph_index += 1 diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_utils.py b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py deleted file mode 100755 index 8d5baae7..00000000 --- a/io_scene_gltf2/blender/exp/gltf2_blender_utils.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2018 The glTF-Blender-IO authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -from io_scene_gltf2.io.com import gltf2_io_constants - - -# TODO: we could apply functional programming to these problems (currently we only have a single use case) - -def split_list_by_data_type(l: list, data_type: gltf2_io_constants.DataType): - """ - Split a flat list of components by their data type. - - E.g.: A list [0,1,2,3,4,5] of data type Vec3 would be split to [[0,1,2], [3,4,5]] - :param l: the flat list - :param data_type: the data type of the list - :return: a list of lists, where each element list contains the components of the data type - """ - if not (len(l) % gltf2_io_constants.DataType.num_elements(data_type) == 0): - raise ValueError("List length does not match specified data type") - num_elements = gltf2_io_constants.DataType.num_elements(data_type) - return [l[i:i + num_elements] for i in range(0, len(l), num_elements)] - - -def max_components(l: list, data_type: gltf2_io_constants.DataType) -> list: - """ - Find the maximum components in a flat list. - - This is required, for example, for the glTF2.0 accessor min and max properties - :param l: the flat list of components - :param data_type: the data type of the list (determines the length of the result) - :return: a list with length num_elements(data_type) containing the maximum per component along the list - """ - components_lists = split_list_by_data_type(l, data_type) - result = [-math.inf] * gltf2_io_constants.DataType.num_elements(data_type) - for components in components_lists: - for i, c in enumerate(components): - result[i] = max(result[i], c) - return result - - -def min_components(l: list, data_type: gltf2_io_constants.DataType) -> list: - """ - Find the minimum components in a flat list. - - This is required, for example, for the glTF2.0 accessor min and max properties - :param l: the flat list of components - :param data_type: the data type of the list (determines the length of the result) - :return: a list with length num_elements(data_type) containing the minimum per component along the list - """ - components_lists = split_list_by_data_type(l, data_type) - result = [math.inf] * gltf2_io_constants.DataType.num_elements(data_type) - for components in components_lists: - for i, c in enumerate(components): - result[i] = min(result[i], c) - return result diff --git a/io_scene_gltf2/io/com/gltf2_io_constants.py b/io_scene_gltf2/io/com/gltf2_io_constants.py index 873e004e..983fe9ab 100755 --- a/io_scene_gltf2/io/com/gltf2_io_constants.py +++ b/io_scene_gltf2/io/com/gltf2_io_constants.py @@ -34,6 +34,18 @@ class ComponentType(IntEnum): ComponentType.Float: 'f' }[component_type] + @classmethod + def to_numpy_dtype(cls, component_type): + import numpy as np + return { + ComponentType.Byte: np.int8, + ComponentType.UnsignedByte: np.uint8, + ComponentType.Short: np.int16, + ComponentType.UnsignedShort: np.uint16, + ComponentType.UnsignedInt: np.uint32, + ComponentType.Float: np.float32, + }[component_type] + @classmethod def from_legacy_define(cls, type_define): return { -- cgit v1.2.3 From bb4dc6f1daab2cc19c79e981222387069fb97ec4 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:21:30 +0200 Subject: glTF importer: performance: rewrite importer using numpy Thanks scurest! --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/imp/gltf2_blender_gltf.py | 4 +- io_scene_gltf2/blender/imp/gltf2_blender_mesh.py | 596 +++++++++++++++++---- .../blender/imp/gltf2_blender_primitive.py | 344 ------------ io_scene_gltf2/io/imp/gltf2_io_binary.py | 136 +++-- io_scene_gltf2/io/imp/gltf2_io_gltf.py | 1 + 6 files changed, 583 insertions(+), 500 deletions(-) delete mode 100755 io_scene_gltf2/blender/imp/gltf2_blender_primitive.py diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 0beb10a1..6404fc20 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 34), + "version": (1, 3, 35), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py index efa7f003..226720a3 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py @@ -50,9 +50,9 @@ class BlenderGlTF(): @staticmethod def set_convert_functions(gltf): - yup2zup = bpy.app.debug_value != 100 + gltf.yup2zup = bpy.app.debug_value != 100 - if yup2zup: + if gltf.yup2zup: # glTF Y-Up space --> Blender Z-up space # X,Y,Z --> X,-Z,Y def convert_loc(x): return Vector([x[0], -x[2], x[1]]) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py index 7914a41b..33578de8 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -13,11 +13,12 @@ # limitations under the License. import bpy -import bmesh +from mathutils import Vector, Matrix +import numpy as np +from ...io.imp.gltf2_io_binary import BinaryData from ..com.gltf2_blender_extras import set_extras from .gltf2_blender_material import BlenderMaterial -from .gltf2_blender_primitive import BlenderPrimitive class BlenderMesh(): @@ -28,118 +29,511 @@ class BlenderMesh(): @staticmethod def create(gltf, mesh_idx, skin_idx): """Mesh creation.""" - pymesh = gltf.data.meshes[mesh_idx] + return create_mesh(gltf, mesh_idx, skin_idx) - # Create one bmesh, add all primitives to it, and then convert it to a - # mesh. - bme = bmesh.new() - # List of all the materials this mesh will use. The material each - # primitive uses is set by giving an index into this list. - materials = [] +# Maximum number of TEXCOORD_n/COLOR_n sets to import +UV_MAX = 8 +COLOR_MAX = 8 - # Process all primitives - for prim in pymesh.primitives: - if prim.material is None: - material_idx = None - else: - pymaterial = gltf.data.materials[prim.material] - - vertex_color = None - if 'COLOR_0' in prim.attributes: - vertex_color = 'COLOR_0' - # Create Blender material if needed - if vertex_color not in pymaterial.blender_material: - BlenderMaterial.create(gltf, prim.material, vertex_color) - material_name = pymaterial.blender_material[vertex_color] - material = bpy.data.materials[material_name] +def create_mesh(gltf, mesh_idx, skin_idx): + pymesh = gltf.data.meshes[mesh_idx] + name = pymesh.name or 'Mesh_%d' % mesh_idx + mesh = bpy.data.meshes.new(name) - try: - material_idx = materials.index(material.name) - except ValueError: - materials.append(material.name) - material_idx = len(materials) - 1 + # Temporarily parent the mesh to an object. + # This is used to set skin weights and shapekeys. + tmp_ob = None + try: + tmp_ob = bpy.data.objects.new('##gltf-import:tmp-object##', mesh) + do_primitives(gltf, mesh_idx, skin_idx, mesh, tmp_ob) - BlenderPrimitive.add_primitive_to_bmesh(gltf, bme, pymesh, prim, skin_idx, material_idx) + finally: + if tmp_ob: + bpy.data.objects.remove(tmp_ob) - name = pymesh.name or 'Mesh_' + str(mesh_idx) - mesh = bpy.data.meshes.new(name) - BlenderMesh.bmesh_to_mesh(gltf, pymesh, bme, mesh) - bme.free() - for name_material in materials: - mesh.materials.append(bpy.data.materials[name_material]) - mesh.update() + return mesh - set_extras(mesh, pymesh.extras, exclude=['targetNames']) - # Clear accessor cache after all primitives are done - gltf.accessor_cache = {} +def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob): + """Put all primitive data into the mesh.""" + pymesh = gltf.data.meshes[mesh_idx] - return mesh + # Scan the primitives to find out what we need to create - @staticmethod - def bmesh_to_mesh(gltf, pymesh, bme, mesh): - bme.to_mesh(mesh) - - # Unfortunately need to do shapekeys/normals/smoothing ourselves. - - # Shapekeys - if len(bme.verts.layers.shape) != 0: - # The only way I could find to create a shape key was to temporarily - # parent mesh to an object and use obj.shape_key_add. - tmp_ob = None - try: - tmp_ob = bpy.data.objects.new('##gltf-import:tmp-object##', mesh) - tmp_ob.shape_key_add(name='Basis') - mesh.shape_keys.name = mesh.name - for layer_name in bme.verts.layers.shape.keys(): - tmp_ob.shape_key_add(name=layer_name) - key_block = mesh.shape_keys.key_blocks[layer_name] - layer = bme.verts.layers.shape[layer_name] - - for i, v in enumerate(bme.verts): - key_block.data[i].co = v[layer] - finally: - if tmp_ob: - bpy.data.objects.remove(tmp_ob) - - # Normals - mesh.update() + has_normals = False + num_uvs = 0 + num_cols = 0 + num_joint_sets = 0 + for prim in pymesh.primitives: + if 'POSITION' not in prim.attributes: + continue if gltf.import_settings['import_shading'] == "NORMALS": - mesh.create_normals_split() - - use_smooths = [] # whether to smooth for each poly - face_idx = 0 - for prim in pymesh.primitives: - if gltf.import_settings['import_shading'] == "FLAT" or \ - 'NORMAL' not in prim.attributes: - use_smooths += [False] * prim.num_faces - elif gltf.import_settings['import_shading'] == "SMOOTH": - use_smooths += [True] * prim.num_faces - elif gltf.import_settings['import_shading'] == "NORMALS": - mesh_loops = mesh.loops - for fi in range(face_idx, face_idx + prim.num_faces): - poly = mesh.polygons[fi] - # "Flat normals" are when all the vertices in poly have the - # poly's normal. Otherwise, smooth the poly. - for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total): - vi = mesh_loops[loop_idx].vertex_index - if poly.normal.dot(bme.verts[vi].normal) <= 0.9999999: - use_smooths.append(True) - break - else: - use_smooths.append(False) + if 'NORMAL' in prim.attributes: + has_normals = True + + if skin_idx is not None: + i = 0 + while ('JOINTS_%d' % i) in prim.attributes and \ + ('WEIGHTS_%d' % i) in prim.attributes: + i += 1 + num_joint_sets = max(i, num_joint_sets) + + i = 0 + while i < UV_MAX and ('TEXCOORD_%d' % i) in prim.attributes: i += 1 + num_uvs = max(i, num_uvs) + + i = 0 + while i < COLOR_MAX and ('COLOR_%d' % i) in prim.attributes: i += 1 + num_cols = max(i, num_cols) + + num_shapekeys = 0 + for morph_i, _ in enumerate(pymesh.primitives[0].targets or []): + if pymesh.shapekey_names[morph_i] is not None: + num_shapekeys += 1 + + # ------------- + # We'll process all the primitives gathering arrays to feed into the + # various foreach_set function that create the mesh data. + + num_faces = 0 # total number of faces + vert_locs = np.empty(dtype=np.float32, shape=(0,3)) # coordinate for each vert + vert_normals = np.empty(dtype=np.float32, shape=(0,3)) # normal for each vert + edge_vidxs = np.array([], dtype=np.uint32) # vertex_index for each loose edge + loop_vidxs = np.array([], dtype=np.uint32) # vertex_index for each loop + loop_uvs = [ + np.empty(dtype=np.float32, shape=(0,2)) # UV for each loop for each layer + for _ in range(num_uvs) + ] + loop_cols = [ + np.empty(dtype=np.float32, shape=(0,4)) # color for each loop for each layer + for _ in range(num_cols) + ] + vert_joints = [ + np.empty(dtype=np.uint32, shape=(0,4)) # 4 joints for each vert for each set + for _ in range(num_joint_sets) + ] + vert_weights = [ + np.empty(dtype=np.float32, shape=(0,4)) # 4 weights for each vert for each set + for _ in range(num_joint_sets) + ] + sk_vert_locs = [ + np.empty(dtype=np.float32, shape=(0,3)) # coordinate for each vert for each shapekey + for _ in range(num_shapekeys) + ] + + for prim in pymesh.primitives: + prim.num_faces = 0 + + if 'POSITION' not in prim.attributes: + continue + + vert_index_base = len(vert_locs) + + if prim.indices is not None: + indices = BinaryData.decode_accessor(gltf, prim.indices) + indices = indices.reshape(len(indices)) + else: + num_verts = gltf.data.accessors[prim.attributes['POSITION']].count + indices = np.arange(0, num_verts, dtype=np.uint32) + + mode = 4 if prim.mode is None else prim.mode + points, edges, tris = points_edges_tris(mode, indices) + if points is not None: + indices = points + elif edges is not None: + indices = edges + else: + indices = tris + + # We'll add one vert to the arrays for each index used in indices + unique_indices, inv_indices = np.unique(indices, return_inverse=True) + + vs = BinaryData.decode_accessor(gltf, prim.attributes['POSITION'], cache=True) + vert_locs = np.concatenate((vert_locs, vs[unique_indices])) + + if has_normals: + if 'NORMAL' in prim.attributes: + ns = BinaryData.decode_accessor(gltf, prim.attributes['NORMAL'], cache=True) + ns = ns[unique_indices] + else: + ns = np.zeros((len(unique_indices), 3), dtype=np.float32) + vert_normals = np.concatenate((vert_normals, ns)) + + for i in range(num_joint_sets): + if ('JOINTS_%d' % i) in prim.attributes and ('WEIGHTS_%d' % i) in prim.attributes: + js = BinaryData.decode_accessor(gltf, prim.attributes['JOINTS_%d' % i], cache=True) + ws = BinaryData.decode_accessor(gltf, prim.attributes['WEIGHTS_%d' % i], cache=True) + js = js[unique_indices] + ws = ws[unique_indices] else: - # shouldn't happen - assert False + js = np.zeros((len(unique_indices), 4), dtype=np.uint32) + ws = np.zeros((len(unique_indices), 4), dtype=np.float32) + vert_joints[i] = np.concatenate((vert_joints[i], js)) + vert_weights[i] = np.concatenate((vert_weights[i], ws)) - face_idx += prim.num_faces - mesh.polygons.foreach_set('use_smooth', use_smooths) + for morph_i, target in enumerate(prim.targets or []): + if pymesh.shapekey_names[morph_i] is None: + continue + morph_vs = BinaryData.decode_accessor(gltf, target['POSITION'], cache=True) + morph_vs = morph_vs[unique_indices] + sk_vert_locs[morph_i] = np.concatenate((sk_vert_locs[morph_i], morph_vs)) - # Custom normals, now that every update is done - if gltf.import_settings['import_shading'] == "NORMALS": - custom_normals = [v.normal for v in bme.verts] - mesh.normals_split_custom_set_from_vertices(custom_normals) - mesh.use_auto_smooth = True + # inv_indices are the indices into the verts just for this prim; + # calculate indices into the overall verts array + prim_vidxs = inv_indices.astype(np.uint32, copy=False) + prim_vidxs += vert_index_base # offset for verts from previous prims + + if edges is not None: + edge_vidxs = np.concatenate((edge_vidxs, prim_vidxs)) + + if tris is not None: + prim.num_faces = len(indices) // 3 + num_faces += prim.num_faces + + loop_vidxs = np.concatenate((loop_vidxs, prim_vidxs)) + + for uv_i in range(num_uvs): + if ('TEXCOORD_%d' % uv_i) in prim.attributes: + uvs = BinaryData.decode_accessor(gltf, prim.attributes['TEXCOORD_%d' % uv_i], cache=True) + uvs = uvs[indices] + else: + uvs = np.zeros((len(indices), 3), dtype=np.float32) + loop_uvs[uv_i] = np.concatenate((loop_uvs[uv_i], uvs)) + + for col_i in range(num_cols): + if ('COLOR_%d' % col_i) in prim.attributes: + cols = BinaryData.decode_accessor(gltf, prim.attributes['COLOR_%d' % col_i], cache=True) + cols = cols[indices] + if cols.shape[1] == 3: + cols = colors_rgb_to_rgba(cols) + else: + cols = np.ones((len(indices), 4), dtype=np.float32) + loop_cols[col_i] = np.concatenate((loop_cols[col_i], cols)) + + # Accessors are cached in case they are shared between primitives; clear + # the cache now that all prims are done. + gltf.decode_accessor_cache = {} + + # --------------- + # Convert all the arrays glTF -> Blender + + # Change from relative to absolute positions for morph locs + for sk_locs in sk_vert_locs: + sk_locs += vert_locs + + if gltf.yup2zup: + locs_yup_to_zup(vert_locs) + locs_yup_to_zup(vert_normals) + for sk_locs in sk_vert_locs: + locs_yup_to_zup(sk_locs) + + if num_joint_sets: + skin_into_bind_pose( + gltf, skin_idx, vert_joints, vert_weights, + locs=[vert_locs] + sk_vert_locs, + vert_normals=vert_normals, + ) + + for uvs in loop_uvs: + uvs_gltf_to_blender(uvs) + + for cols in loop_cols: + colors_linear_to_srgb(cols[:, :-1]) + + # --------------- + # Start creating things + + mesh.vertices.add(len(vert_locs)) + mesh.vertices.foreach_set('co', squish(vert_locs)) + + mesh.loops.add(len(loop_vidxs)) + mesh.loops.foreach_set('vertex_index', loop_vidxs) + + mesh.edges.add(len(edge_vidxs) // 2) + mesh.edges.foreach_set('vertices', edge_vidxs) + + mesh.polygons.add(num_faces) + + # All polys are tris + loop_starts = np.arange(0, 3 * num_faces, step=3) + loop_totals = np.full(num_faces, 3) + mesh.polygons.foreach_set('loop_start', loop_starts) + mesh.polygons.foreach_set('loop_total', loop_totals) + + for uv_i in range(num_uvs): + name = 'UVMap' if uv_i == 0 else 'UVMap.%03d' % uv_i + layer = mesh.uv_layers.new(name=name) + layer.data.foreach_set('uv', squish(loop_uvs[uv_i])) + + for col_i in range(num_cols): + name = 'Col' if col_i == 0 else 'Col.%03d' % col_i + layer = mesh.vertex_colors.new(name=name) + + layer.data.foreach_set('color', squish(loop_cols[col_i])) + + # Skinning + # TODO: this is slow :/ + if num_joint_sets: + pyskin = gltf.data.skins[skin_idx] + for i, _ in enumerate(pyskin.joints): + # ob is a temp object, so don't worry about the name. + ob.vertex_groups.new(name='X%d' % i) + + vgs = list(ob.vertex_groups) + + for i in range(num_joint_sets): + js = vert_joints[i].tolist() # tolist() is faster + ws = vert_weights[i].tolist() + for vi in range(len(vert_locs)): + w0, w1, w2, w3 = ws[vi] + j0, j1, j2, j3 = js[vi] + if w0 != 0: vgs[j0].add((vi,), w0, 'REPLACE') + if w1 != 0: vgs[j1].add((vi,), w1, 'REPLACE') + if w2 != 0: vgs[j2].add((vi,), w2, 'REPLACE') + if w3 != 0: vgs[j3].add((vi,), w3, 'REPLACE') + + # Shapekeys + if num_shapekeys: + ob.shape_key_add(name='Basis') + mesh.shape_keys.name = mesh.name + + sk_i = 0 + for sk_name in pymesh.shapekey_names: + if sk_name is None: + continue + + ob.shape_key_add(name=sk_name) + key_block = mesh.shape_keys.key_blocks[sk_name] + key_block.data.foreach_set('co', squish(sk_vert_locs[sk_i])) + + sk_i += 1 + + # ---- + # Assign materials to faces + + # Initialize to no-material, ie. an index guaranteed to be OOB for the + # material slots. A mesh obviously can't have more materials than it has + # primitives... + oob_material_idx = len(pymesh.primitives) + material_indices = np.full(num_faces, oob_material_idx) + + f = 0 + for prim in pymesh.primitives: + if prim.material is not None: + # Get the material + pymaterial = gltf.data.materials[prim.material] + vertex_color = 'COLOR_0' if 'COLOR_0' in prim.attributes else None + if vertex_color not in pymaterial.blender_material: + BlenderMaterial.create(gltf, prim.material, vertex_color) + material_name = pymaterial.blender_material[vertex_color] + + # Put material in slot (if not there) + if material_name not in mesh.materials: + mesh.materials.append(bpy.data.materials[material_name]) + material_index = mesh.materials.find(material_name) + + material_indices[f:f + prim.num_faces].fill(material_index) + + f += prim.num_faces + + mesh.polygons.foreach_set('material_index', material_indices) + + # ---- + # Normals + + # Set poly smoothing + # TODO: numpyify? + smooths = [] # use_smooth for each poly + f = 0 + for prim in pymesh.primitives: + if gltf.import_settings['import_shading'] == "FLAT" or \ + 'NORMAL' not in prim.attributes: + smooths += [False] * prim.num_faces + + elif gltf.import_settings['import_shading'] == "SMOOTH": + smooths += [True] * prim.num_faces + + elif gltf.import_settings['import_shading'] == "NORMALS": + for fi in range(f, f + prim.num_faces): + # Make the face flat if the face's normal is + # equal to all of its loops' normals. + poly_normal = mesh.polygons[fi].normal + smooths.append( + poly_normal.dot(vert_normals[loop_vidxs[3*fi + 0]]) <= 0.9999999 or + poly_normal.dot(vert_normals[loop_vidxs[3*fi + 1]]) <= 0.9999999 or + poly_normal.dot(vert_normals[loop_vidxs[3*fi + 2]]) <= 0.9999999 + ) + + f += prim.num_faces + + mesh.polygons.foreach_set('use_smooth', smooths) + + mesh.validate() + has_loose_edges = len(edge_vidxs) != 0 # need to calc_loose_edges for them to show up + mesh.update(calc_edges_loose=has_loose_edges) + + if has_normals: + mesh.create_normals_split() + mesh.normals_split_custom_set_from_vertices(vert_normals) + mesh.use_auto_smooth = True + + +def points_edges_tris(mode, indices): + points = None + edges = None + tris = None + + if mode == 0: + # POINTS + points = indices + + elif mode == 1: + # LINES + # 1 3 + # / / + # 0 2 + edges = indices + + elif mode == 2: + # LINE LOOP + # 1---2 + # / \ + # 0-------3 + # in: 0123 + # out: 01122330 + edges = np.empty(2 * len(indices), dtype=np.uint32) + edges[[0, -1]] = indices[[0, 0]] # 0______0 + edges[1:-1] = np.repeat(indices[1:], 2) # 01122330 + + elif mode == 3: + # LINE STRIP + # 1---2 + # / \ + # 0 3 + # in: 0123 + # out: 011223 + edges = np.empty(2 * len(indices) - 2, dtype=np.uint32) + edges[[0, -1]] = indices[[0, -1]] # 0____3 + edges[1:-1] = np.repeat(indices[1:-1], 2) # 011223 + + elif mode == 4: + # TRIANGLES + # 2 3 + # / \ / \ + # 0---1 4---5 + tris = indices + + elif mode == 5: + # TRIANGLE STRIP + # 0---2---4 + # \ / \ / + # 1---3 + # TODO: numpyify + def alternate(i, xs): + even = i % 2 == 0 + return xs if even else (xs[0], xs[2], xs[1]) + tris = np.array([ + alternate(i, (indices[i], indices[i + 1], indices[i + 2])) + for i in range(0, len(indices) - 2) + ]) + tris = squish(tris) + + elif mode == 6: + # TRIANGLE FAN + # 3---2 + # / \ / \ + # 4---0---1 + # TODO: numpyify + tris = np.array([ + (indices[0], indices[i], indices[i + 1]) + for i in range(1, len(indices) - 1) + ]) + tris = squish(tris) + + else: + raise Exception('primitive mode unimplemented: %d' % mode) + + return points, edges, tris + + +def squish(array): + """Squish nD array into 1D array (required by foreach_set).""" + return array.reshape(array.size) + + +def colors_rgb_to_rgba(rgb): + rgba = np.ones((len(rgb), 4), dtype=np.float32) + rgba[:, :3] = rgb + return rgba + + +def colors_linear_to_srgb(color): + assert color.shape[1] == 3 # only change RGB, not A + + not_small = color >= 0.0031308 + small_result = np.where(color < 0.0, 0.0, color * 12.92) + large_result = 1.055 * np.power(color, 1.0 / 2.4, where=not_small) - 0.055 + color[:] = np.where(not_small, large_result, small_result) + + +def locs_yup_to_zup(vecs): + # x,y,z -> x,-z,y + vecs[:, [1,2]] = vecs[:, [2,1]] + vecs[:, 1] *= -1 + + +def uvs_gltf_to_blender(uvs): + # u,v -> u,1-v + uvs[:, 1] *= -1 + uvs[:, 1] += 1 + + +def skin_into_bind_pose(gltf, skin_idx, vert_joints, vert_weights, locs, vert_normals): + # Skin each position/normal using the bind pose. + # Skinning equation: vert' = sum_(j,w) w * joint_mat[j] * vert + # where the sum is over all (joint,weight) pairs. + + # Calculate joint matrices + joint_mats = [] + pyskin = gltf.data.skins[skin_idx] + if pyskin.inverse_bind_matrices is not None: + inv_binds = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices) + inv_binds = [gltf.matrix_gltf_to_blender(m) for m in inv_binds] + else: + inv_binds = [Matrix.Identity(4) for i in range(len(pyskin.joints))] + bind_mats = [gltf.vnodes[joint].bind_arma_mat for joint in pyskin.joints] + joint_mats = [bind_mat @ inv_bind for bind_mat, inv_bind in zip(bind_mats, inv_binds)] + + # TODO: check if joint_mats are all (approximately) 1, and skip skinning + + joint_mats = np.array(joint_mats, dtype=np.float32) + + # Compute the skinning matrices for every vert + num_verts = len(locs[0]) + skinning_mats = np.zeros((num_verts, 4, 4), dtype=np.float32) + weight_sums = np.zeros(num_verts, dtype=np.float32) + for js, ws in zip(vert_joints, vert_weights): + for i in range(4): + skinning_mats += ws[:, i].reshape(len(ws), 1, 1) * joint_mats[js[:, i]] + weight_sums += ws[:, i] + # Normalize weights to one; necessary for old files / quantized weights + skinning_mats /= weight_sums.reshape(num_verts, 1, 1) + + skinning_mats_3x3 = skinning_mats[:, :3, :3] + skinning_trans = skinning_mats[:, :3, 3] + + for vs in locs: + vs[:] = mul_mats_vecs(skinning_mats_3x3, vs) + vs[:] += skinning_trans + + if len(vert_normals) != 0: + vert_normals[:] = mul_mats_vecs(skinning_mats_3x3, vert_normals) + # Don't translate normals! + + +def mul_mats_vecs(mats, vecs): + """Given [m1,m2,...] and [v1,v2,...], returns [m1@v1,m2@v2,...]. 3D only.""" + return np.matmul(mats, vecs.reshape(len(vecs), 3, 1)).reshape(len(vecs), 3) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py deleted file mode 100755 index d544778c..00000000 --- a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py +++ /dev/null @@ -1,344 +0,0 @@ -# Copyright 2018-2019 The glTF-Blender-IO authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bpy -from mathutils import Vector, Matrix - -from ...io.imp.gltf2_io_binary import BinaryData -from ...io.com.gltf2_io_color_management import color_linear_to_srgb -from ...io.com import gltf2_io_debug - - -MAX_NUM_COLOR_SETS = 8 -MAX_NUM_TEXCOORD_SETS = 8 - -class BlenderPrimitive(): - """Blender Primitive.""" - def __new__(cls, *args, **kwargs): - raise RuntimeError("%s should not be instantiated" % cls) - - @staticmethod - def get_layer(bme_layers, name): - if name not in bme_layers: - return bme_layers.new(name) - return bme_layers[name] - - @staticmethod - def add_primitive_to_bmesh(gltf, bme, pymesh, pyprimitive, skin_idx, material_index): - attributes = pyprimitive.attributes - - if 'POSITION' not in attributes: - pyprimitive.num_faces = 0 - return - - positions = BinaryData.get_data_from_accessor(gltf, attributes['POSITION'], cache=True) - - if pyprimitive.indices is not None: - # Not using cache, this is not useful for indices - indices = BinaryData.get_data_from_accessor(gltf, pyprimitive.indices) - indices = [i[0] for i in indices] - else: - indices = list(range(len(positions))) - - bme_verts = bme.verts - bme_edges = bme.edges - bme_faces = bme.faces - - # Gather up the joints/weights (multiple sets allow >4 influences) - joint_sets = [] - weight_sets = [] - set_num = 0 - while 'JOINTS_%d' % set_num in attributes and 'WEIGHTS_%d' % set_num in attributes: - joint_data = BinaryData.get_data_from_accessor(gltf, attributes['JOINTS_%d' % set_num], cache=True) - weight_data = BinaryData.get_data_from_accessor(gltf, attributes['WEIGHTS_%d' % set_num], cache=True) - - joint_sets.append(joint_data) - weight_sets.append(weight_data) - - set_num += 1 - - # For skinned meshes, we will need to calculate the position of the - # verts in the bind pose, ie. the pose the edit bones are in. - if skin_idx is not None: - pyskin = gltf.data.skins[skin_idx] - if pyskin.inverse_bind_matrices is not None: - inv_binds = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices) - inv_binds = [gltf.matrix_gltf_to_blender(m) for m in inv_binds] - else: - inv_binds = [Matrix.Identity(4) for i in range(len(pyskin.joints))] - bind_mats = [gltf.vnodes[joint].bind_arma_mat for joint in pyskin.joints] - joint_mats = [bind_mat @ inv_bind for bind_mat, inv_bind in zip(bind_mats, inv_binds)] - - def skin_vert(pos, pidx): - out = Vector((0, 0, 0)) - # Spec says weights should already sum to 1 but some models - # don't do it (ex. CesiumMan), so normalize. - weight_sum = 0 - for joint_set, weight_set in zip(joint_sets, weight_sets): - for j in range(0, 4): - weight = weight_set[pidx][j] - if weight != 0.0: - weight_sum += weight - joint = joint_set[pidx][j] - out += weight * (joint_mats[joint] @ pos) - out /= weight_sum - return out - - def skin_normal(norm, pidx): - # TODO: not sure this is right - norm = Vector([norm[0], norm[1], norm[2], 0]) - out = Vector((0, 0, 0, 0)) - weight_sum = 0 - for joint_set, weight_set in zip(joint_sets, weight_sets): - for j in range(0, 4): - weight = weight_set[pidx][j] - if weight != 0.0: - weight_sum += weight - joint = joint_set[pidx][j] - out += weight * (joint_mats[joint] @ norm) - out /= weight_sum - out = out.to_3d().normalized() - return out - - # Every vertex has an index into the primitive's attribute arrays and a - # *different* index into the BMesh's list of verts. Call the first one the - # pidx and the second the bidx. Need to keep them straight! - - # The pidx of all the vertices that are actually used by the primitive (only - # indices that appear in the pyprimitive.indices list are actually used) - used_pidxs = set(indices) - # Contains a pair (bidx, pidx) for every vertex in the primitive - vert_idxs = [] - # pidx_to_bidx[pidx] will be the bidx of the vertex with that pidx (or -1 if - # unused) - pidx_to_bidx = [-1] * len(positions) - bidx = len(bme_verts) - if bpy.app.debug: - used_pidxs = list(used_pidxs) - used_pidxs.sort() - for pidx in used_pidxs: - pos = gltf.loc_gltf_to_blender(positions[pidx]) - if skin_idx is not None: - pos = skin_vert(pos, pidx) - - bme_verts.new(pos) - vert_idxs.append((bidx, pidx)) - pidx_to_bidx[pidx] = bidx - bidx += 1 - bme_verts.ensure_lookup_table() - - # Add edges/faces to bmesh - mode = 4 if pyprimitive.mode is None else pyprimitive.mode - edges, faces = BlenderPrimitive.edges_and_faces(mode, indices) - # NOTE: edges and vertices are in terms of pidxs! - for edge in edges: - try: - bme_edges.new(( - bme_verts[pidx_to_bidx[edge[0]]], - bme_verts[pidx_to_bidx[edge[1]]], - )) - except ValueError: - # Ignores duplicate/degenerate edges - pass - pyprimitive.num_faces = 0 - for face in faces: - try: - face = bme_faces.new(( - bme_verts[pidx_to_bidx[face[0]]], - bme_verts[pidx_to_bidx[face[1]]], - bme_verts[pidx_to_bidx[face[2]]], - )) - - if material_index is not None: - face.material_index = material_index - - pyprimitive.num_faces += 1 - - except ValueError: - # Ignores duplicate/degenerate faces - pass - - # Set normals - if 'NORMAL' in attributes: - normals = BinaryData.get_data_from_accessor(gltf, attributes['NORMAL'], cache=True) - - if skin_idx is None: - for bidx, pidx in vert_idxs: - bme_verts[bidx].normal = gltf.normal_gltf_to_blender(normals[pidx]) - else: - for bidx, pidx in vert_idxs: - normal = gltf.normal_gltf_to_blender(normals[pidx]) - bme_verts[bidx].normal = skin_normal(normal, pidx) - - # Set vertex colors. Add them in the order COLOR_0, COLOR_1, etc. - set_num = 0 - while 'COLOR_%d' % set_num in attributes: - if set_num >= MAX_NUM_COLOR_SETS: - gltf2_io_debug.print_console("WARNING", - "too many color sets; COLOR_%d will be ignored" % set_num - ) - break - - layer_name = 'Col' if set_num == 0 else 'Col.%03d' % set_num - layer = BlenderPrimitive.get_layer(bme.loops.layers.color, layer_name) - - colors = BinaryData.get_data_from_accessor(gltf, attributes['COLOR_%d' % set_num], cache=True) - is_rgba = len(colors[0]) == 4 - - for bidx, pidx in vert_idxs: - color = colors[pidx] - col = ( - color_linear_to_srgb(color[0]), - color_linear_to_srgb(color[1]), - color_linear_to_srgb(color[2]), - color[3] if is_rgba else 1.0, - ) - for loop in bme_verts[bidx].link_loops: - loop[layer] = col - - set_num += 1 - - # Set texcoords - set_num = 0 - while 'TEXCOORD_%d' % set_num in attributes: - if set_num >= MAX_NUM_TEXCOORD_SETS: - gltf2_io_debug.print_console("WARNING", - "too many UV sets; TEXCOORD_%d will be ignored" % set_num - ) - break - - layer_name = 'UVMap' if set_num == 0 else 'UVMap.%03d' % set_num - layer = BlenderPrimitive.get_layer(bme.loops.layers.uv, layer_name) - - uvs = BinaryData.get_data_from_accessor(gltf, attributes['TEXCOORD_%d' % set_num], cache=True) - - for bidx, pidx in vert_idxs: - # UV transform - u, v = uvs[pidx] - uv = (u, 1 - v) - - for loop in bme_verts[bidx].link_loops: - loop[layer].uv = uv - - set_num += 1 - - # Set joints/weights for skinning - if joint_sets: - layer = BlenderPrimitive.get_layer(bme.verts.layers.deform, 'Vertex Weights') - - for joint_set, weight_set in zip(joint_sets, weight_sets): - for bidx, pidx in vert_idxs: - for j in range(0, 4): - weight = weight_set[pidx][j] - if weight != 0.0: - joint = joint_set[pidx][j] - bme_verts[bidx][layer][joint] = weight - - # Set morph target positions (no normals/tangents) - for sk, target in enumerate(pyprimitive.targets or []): - if pymesh.shapekey_names[sk] is None: - continue - - layer_name = pymesh.shapekey_names[sk] - layer = BlenderPrimitive.get_layer(bme.verts.layers.shape, layer_name) - - morph_positions = BinaryData.get_data_from_accessor(gltf, target['POSITION'], cache=True) - - if skin_idx is None: - for bidx, pidx in vert_idxs: - bme_verts[bidx][layer] = ( - gltf.loc_gltf_to_blender(positions[pidx]) + - gltf.loc_gltf_to_blender(morph_positions[pidx]) - ) - else: - for bidx, pidx in vert_idxs: - pos = ( - gltf.loc_gltf_to_blender(positions[pidx]) + - gltf.loc_gltf_to_blender(morph_positions[pidx]) - ) - bme_verts[bidx][layer] = skin_vert(pos, pidx) - - @staticmethod - def edges_and_faces(mode, indices): - """Converts the indices in a particular primitive mode into standard lists of - edges (pairs of indices) and faces (tuples of CCW indices). - """ - es = [] - fs = [] - - if mode == 0: - # POINTS - pass - elif mode == 1: - # LINES - # 1 3 - # / / - # 0 2 - es = [ - (indices[i], indices[i + 1]) - for i in range(0, len(indices), 2) - ] - elif mode == 2: - # LINE LOOP - # 1---2 - # / \ - # 0-------3 - es = [ - (indices[i], indices[i + 1]) - for i in range(0, len(indices) - 1) - ] - es.append((indices[-1], indices[0])) - elif mode == 3: - # LINE STRIP - # 1---2 - # / \ - # 0 3 - es = [ - (indices[i], indices[i + 1]) - for i in range(0, len(indices) - 1) - ] - elif mode == 4: - # TRIANGLES - # 2 3 - # / \ / \ - # 0---1 4---5 - fs = [ - (indices[i], indices[i + 1], indices[i + 2]) - for i in range(0, len(indices), 3) - ] - elif mode == 5: - # TRIANGLE STRIP - # 0---2---4 - # \ / \ / - # 1---3 - def alternate(i, xs): - even = i % 2 == 0 - return xs if even else (xs[0], xs[2], xs[1]) - fs = [ - alternate(i, (indices[i], indices[i + 1], indices[i + 2])) - for i in range(0, len(indices) - 2) - ] - elif mode == 6: - # TRIANGLE FAN - # 3---2 - # / \ / \ - # 4---0---1 - fs = [ - (indices[0], indices[i], indices[i + 1]) - for i in range(1, len(indices) - 1) - ] - else: - raise Exception('primitive mode unimplemented: %d' % mode) - - return es, fs diff --git a/io_scene_gltf2/io/imp/gltf2_io_binary.py b/io_scene_gltf2/io/imp/gltf2_io_binary.py index 7cfcbc40..728cf0f0 100755 --- a/io_scene_gltf2/io/imp/gltf2_io_binary.py +++ b/io_scene_gltf2/io/imp/gltf2_io_binary.py @@ -13,6 +13,7 @@ # limitations under the License. import struct +import numpy as np from ..com.gltf2_io import Accessor @@ -22,8 +23,8 @@ class BinaryData(): def __new__(cls, *args, **kwargs): raise RuntimeError("%s should not be instantiated" % cls) -# Note that this function is not used in Blender importer, but is kept in -# Source code to be used in any pipeline that want to manage gltf/glb file in python + # Note that this function is not used in Blender importer, but is kept in + # Source code to be used in any pipeline that want to manage gltf/glb file in python @staticmethod def get_binary_from_accessor(gltf, accessor_idx): """Get binary from accessor.""" @@ -63,8 +64,7 @@ class BinaryData(): if accessor_idx in gltf.accessor_cache: return gltf.accessor_cache[accessor_idx] - accessor = gltf.data.accessors[accessor_idx] - data = BinaryData.get_data_from_accessor_obj(gltf, accessor) + data = BinaryData.decode_accessor(gltf, accessor_idx).tolist() if cache: gltf.accessor_cache[accessor_idx] = data @@ -72,7 +72,36 @@ class BinaryData(): return data @staticmethod - def get_data_from_accessor_obj(gltf, accessor): + def decode_accessor(gltf, accessor_idx, cache=False): + """Decodes accessor to 2D numpy array (count x num_components).""" + if accessor_idx in gltf.decode_accessor_cache: + return gltf.accessor_cache[accessor_idx] + + accessor = gltf.data.accessors[accessor_idx] + array = BinaryData.decode_accessor_obj(gltf, accessor) + + if cache: + gltf.accessor_cache[accessor_idx] = array + # Prevent accidentally modifying cached arrays + array.flags.writeable = False + + return array + + @staticmethod + def decode_accessor_obj(gltf, accessor): + # MAT2/3 have special alignment requirements that aren't handled. But it + # doesn't matter because nothing uses them. + assert accessor.type not in ['MAT2', 'MAT3'] + + dtype = { + 5120: np.int8, + 5121: np.uint8, + 5122: np.int16, + 5123: np.uint16, + 5125: np.uint32, + 5126: np.float32, + }[accessor.component_type] + if accessor.buffer_view is not None: bufferView = gltf.data.buffer_views[accessor.buffer_view] buffer_data = BinaryData.get_buffer_view(gltf, accessor.buffer_view) @@ -80,40 +109,45 @@ class BinaryData(): accessor_offset = accessor.byte_offset or 0 buffer_data = buffer_data[accessor_offset:] - fmt_char = gltf.fmt_char_dict[accessor.component_type] component_nb = gltf.component_nb_dict[accessor.type] - fmt = '<' + (fmt_char * component_nb) - default_stride = struct.calcsize(fmt) - - # Special layouts for certain formats; see the section about - # data alignment in the glTF 2.0 spec. - component_size = struct.calcsize('<' + fmt_char) - if accessor.type == 'MAT2' and component_size == 1: - fmt = ' Date: Tue, 21 Jul 2020 20:24:03 +0200 Subject: glTF exporter: use mesh.loop_triangles Thanks scurest! --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_extract.py | 30 +++++----------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 6404fc20..7e64f181 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 35), + "version": (1, 3, 36), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index 5fe719ee..704a7b4e 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -17,7 +17,6 @@ # from mathutils import Vector, Quaternion, Matrix -from mathutils.geometry import tessellate_polygon from . import gltf2_blender_export_keys from ...io.com.gltf2_io_debug import print_console @@ -232,7 +231,11 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert prims = {} - for blender_polygon in blender_mesh.polygons: + blender_mesh.calc_loop_triangles() + + for loop_tri in blender_mesh.loop_triangles: + blender_polygon = blender_mesh.polygons[loop_tri.polygon_index] + material_idx = -1 if use_materials: material_idx = blender_polygon.material_index @@ -257,28 +260,7 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert face_tangent.normalize() face_bitangent.normalize() - loop_index_list = [] - - if len(blender_polygon.loop_indices) == 3: - loop_index_list.extend(blender_polygon.loop_indices) - elif len(blender_polygon.loop_indices) > 3: - # Triangulation of polygon. Using internal function, as non-convex polygons could exist. - polyline = [] - - for loop_index in blender_polygon.loop_indices: - vertex_index = blender_mesh.loops[loop_index].vertex_index - v = blender_mesh.vertices[vertex_index].co - polyline.append(Vector((v[0], v[1], v[2]))) - - triangles = tessellate_polygon((polyline,)) - - for triangle in triangles: - for triangle_index in triangle: - loop_index_list.append(blender_polygon.loop_indices[triangle_index]) - else: - continue - - for loop_index in loop_index_list: + for loop_index in loop_tri.loops: vertex_index = blender_mesh.loops[loop_index].vertex_index vertex = blender_mesh.vertices[vertex_index] -- cgit v1.2.3 From 422c47c5f79ed0e693d6b876b232736d34af83d9 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:27:00 +0200 Subject: glTF exporter: use split normals when exporting morph targets Thanks scurest! --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_extract.py | 33 ++++++---------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 7e64f181..02fc114f 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 36), + "version": (1, 3, 37), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index 704a7b4e..c28fddf9 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -34,10 +34,9 @@ class Prim: self.indices = [] class ShapeKey: - def __init__(self, shape_key, vertex_normals, polygon_normals): + def __init__(self, shape_key, split_normals): self.shape_key = shape_key - self.vertex_normals = vertex_normals - self.polygon_normals = polygon_normals + self.split_normals = split_normals # @@ -210,16 +209,14 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert for blender_shape_key in blender_mesh.shape_keys.key_blocks: if blender_shape_key == blender_shape_key.relative_key or blender_shape_key.mute: continue + + split_normals = None if use_morph_normals: - vertex_normals = blender_shape_key.normals_vertex_get() - polygon_normals = blender_shape_key.normals_polygon_get() - else: - vertex_normals = None - polygon_normals = None + split_normals = blender_shape_key.normals_split_get() + shape_keys.append(ShapeKey( blender_shape_key, - vertex_normals, - polygon_normals, + split_normals, )) @@ -330,20 +327,8 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert vert += ((v_morph[0], v_morph[1], v_morph[2]),) if use_morph_normals: - if blender_polygon.use_smooth: - normals = shape_key.vertex_normals - n_morph = Vector(( - normals[vertex_index * 3 + 0], - normals[vertex_index * 3 + 1], - normals[vertex_index * 3 + 2], - )) - else: - normals = shape_key.polygon_normals - n_morph = Vector(( - normals[blender_polygon.index * 3 + 0], - normals[blender_polygon.index * 3 + 1], - normals[blender_polygon.index * 3 + 2], - )) + normals = shape_key.split_normals + n_morph = Vector(normals[loop_index * 3 : loop_index * 3 + 3]) n_morph = n_morph - n # store delta vert += ((n_morph[0], n_morph[1], n_morph[2]),) -- cgit v1.2.3 From 47ea656bdd61e5d19f577b1155789e443e26e3e3 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:28:43 +0200 Subject: glTF exporter: export curve/surface/text objects as meshes Thanks scurest! --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_gather_nodes.py | 46 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 02fc114f..92d828bb 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 37), + "version": (1, 3, 38), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py index 83984c2b..b2ca5be5 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -248,6 +248,9 @@ def __gather_matrix(blender_object, export_settings): def __gather_mesh(blender_object, library, export_settings): + if blender_object.type in ['CURVE', 'SURFACE', 'FONT']: + return __gather_mesh_from_nonmesh(blender_object, library, export_settings) + if blender_object.type != "MESH": return None @@ -338,6 +341,49 @@ def __gather_mesh(blender_object, library, export_settings): return result +def __gather_mesh_from_nonmesh(blender_object, library, export_settings): + """Handles curves, surfaces, text, etc.""" + needs_to_mesh_clear = False + try: + # Convert to a mesh + try: + if export_settings[gltf2_blender_export_keys.APPLY]: + depsgraph = bpy.context.evaluated_depsgraph_get() + blender_mesh_owner = blender_object.evaluated_get(depsgraph) + blender_mesh = blender_mesh_owner.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) + # TODO: do we need preserve_all_data_layers? + + else: + blender_mesh_owner = blender_object + blender_mesh = blender_mesh_owner.to_mesh() + + except Exception: + return None + + needs_to_mesh_clear = True + + skip_filter = True + material_names = tuple([ms.material.name for ms in blender_object.material_slots if ms.material is not None]) + vertex_groups = None + modifiers = None + blender_object_for_skined_data = None + + result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh, + library, + blender_object_for_skined_data, + vertex_groups, + modifiers, + skip_filter, + material_names, + export_settings) + + finally: + if needs_to_mesh_clear: + blender_mesh_owner.to_mesh_clear() + + return result + + def __gather_name(blender_object, export_settings): return blender_object.name -- cgit v1.2.3 From 9313b3a155bbe5b84dca81431ca9167cda94035d Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 20:30:30 +0200 Subject: glTF exporter: refactoring: remove no more needed functions after 2.79 --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/com/gltf2_blender_math.py | 13 ++++--------- io_scene_gltf2/blender/exp/gltf2_blender_extract.py | 6 ------ .../blender/exp/gltf2_blender_gather_animation_samplers.py | 8 +++++--- io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py | 13 ++++++------- io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py | 6 +++--- io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py | 9 ++++----- 7 files changed, 23 insertions(+), 34 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 92d828bb..57bdf3d5 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 38), + "version": (1, 3, 39), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/com/gltf2_blender_math.py b/io_scene_gltf2/blender/com/gltf2_blender_math.py index 72eb124a..bddc79a6 100755 --- a/io_scene_gltf2/blender/com/gltf2_blender_math.py +++ b/io_scene_gltf2/blender/com/gltf2_blender_math.py @@ -19,11 +19,6 @@ from mathutils import Matrix, Vector, Quaternion, Euler from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name -def multiply(a, b): - """Multiplication.""" - return a @ b - - def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Union[Vector, Quaternion, Euler]: """Transform a list to blender py object.""" target = get_target_property_name(data_path) @@ -31,7 +26,7 @@ def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Unio if target == 'delta_location': return Vector(values) # TODO Should be Vector(values) - Vector(something)? elif target == 'delta_rotation_euler': - return Euler(values).to_quaternion() # TODO Should be multiply(Euler(values).to_quaternion(), something)? + return Euler(values).to_quaternion() # TODO Should be Euler(values).to_quaternion() @ something? elif target == 'location': return Vector(values) elif target == 'rotation_axis_angle': @@ -138,7 +133,7 @@ def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Ma def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector: """Transform location.""" m = Matrix.Translation(location) - m = multiply(transform, m) + m = transform @ m return m.to_translation() @@ -146,7 +141,7 @@ def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity """Transform rotation.""" rotation.normalize() m = rotation.to_matrix().to_4x4() - m = multiply(transform, m) + m = transform @ m return m.to_quaternion() @@ -156,7 +151,7 @@ def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Ve m[0][0] = scale.x m[1][1] = scale.y m[2][2] = scale.z - m = multiply(transform, m) + m = transform @ m return m.to_scale() diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index c28fddf9..6389d9dc 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -122,12 +122,6 @@ def convert_swizzle_scale(scale, export_settings): return Vector((scale[0], scale[1], scale[2])) -def decompose_transition(matrix, export_settings): - translation, rotation, scale = matrix.decompose() - - return translation, rotation, scale - - def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vertex_groups, modifiers, export_settings): """ Extract primitives from a mesh. Polygons are triangulated and sorted by material. diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py index f2375bb1..c3913367 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py @@ -343,10 +343,12 @@ def __gather_output(channels: typing.Tuple[bpy.types.FCurve], (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) - correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, bone.bone.matrix_local) + correction_matrix_local = axis_basis_change @ bone.bone.matrix_local else: - correction_matrix_local = gltf2_blender_math.multiply( - bone.parent.bone.matrix_local.inverted(), bone.bone.matrix_local) + correction_matrix_local = ( + bone.parent.bone.matrix_local.inverted() @ + bone.bone.matrix_local + ) transform = correction_matrix_local else: diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py index 1cb26551..dff55d17 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py @@ -17,8 +17,6 @@ import mathutils from . import gltf2_blender_export_keys from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached from io_scene_gltf2.io.com import gltf2_io -from io_scene_gltf2.blender.exp import gltf2_blender_extract -from io_scene_gltf2.blender.com import gltf2_blender_math from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions from ..com.gltf2_blender_extras import generate_extras @@ -39,10 +37,12 @@ def gather_joint(blender_object, blender_bone, export_settings): # extract bone transform if blender_bone.parent is None: - correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local) + correction_matrix_local = axis_basis_change @ blender_bone.bone.matrix_local else: - correction_matrix_local = gltf2_blender_math.multiply( - blender_bone.parent.bone.matrix_local.inverted(), blender_bone.bone.matrix_local) + correction_matrix_local = ( + blender_bone.parent.bone.matrix_local.inverted() @ + blender_bone.bone.matrix_local + ) if (blender_bone.bone.use_inherit_rotation == False or blender_bone.bone.inherit_scale != "FULL") and blender_bone.parent != None: rest_mat = (blender_bone.parent.bone.matrix_local.inverted_safe() @ blender_bone.bone.matrix_local) @@ -51,8 +51,7 @@ def gather_joint(blender_object, blender_bone, export_settings): matrix_basis = blender_bone.matrix matrix_basis = blender_object.convert_space(pose_bone=blender_bone, matrix=matrix_basis, from_space='POSE', to_space='LOCAL') - trans, rot, sca = gltf2_blender_extract.decompose_transition( - gltf2_blender_math.multiply(correction_matrix_local, matrix_basis), export_settings) + trans, rot, sca = (correction_matrix_local @ matrix_basis).decompose() translation, rotation, scale = (None, None, None) if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0: translation = [trans[0], trans[1], trans[2]] diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py index b2ca5be5..b09e7aa1 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -189,8 +189,8 @@ def __gather_children(blender_object, blender_scene, export_settings): rot_quat = Quaternion(rot) axis_basis_change = Matrix( ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, -1.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) - mat = gltf2_blender_math.multiply(child.matrix_parent_inverse, child.matrix_basis) - mat = gltf2_blender_math.multiply(mat, axis_basis_change) + mat = child.matrix_parent_inverse @ child.matrix_basis + mat = mat @ axis_basis_change _, rot_quat, _ = mat.decompose() child_node.rotation = [rot_quat[1], rot_quat[2], rot_quat[3], rot_quat[0]] @@ -404,7 +404,7 @@ def __gather_trans_rot_scale(blender_object, export_settings): if blender_object.matrix_local[3][3] != 0.0: - trans, rot, sca = gltf2_blender_extract.decompose_transition(blender_object.matrix_local, export_settings) + trans, rot, sca = blender_object.matrix_local.decompose() else: # Some really weird cases, scale is null (if parent is null when evaluation is done) print_console('WARNING', 'Some nodes are 0 scaled during evaluation. Result can be wrong') diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py index fa95e543..7f645272 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py @@ -20,7 +20,6 @@ from io_scene_gltf2.io.exp import gltf2_io_binary_data from io_scene_gltf2.io.com import gltf2_io_constants from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints -from io_scene_gltf2.blender.com import gltf2_blender_math from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions @@ -85,10 +84,10 @@ def __gather_inverse_bind_matrices(blender_object, export_settings): # traverse the matrices in the same order as the joints and compute the inverse bind matrix def __collect_matrices(bone): - inverse_bind_matrix = gltf2_blender_math.multiply( - axis_basis_change, - gltf2_blender_math.multiply( - blender_object.matrix_world, + inverse_bind_matrix = ( + axis_basis_change @ + ( + blender_object.matrix_world @ bone.bone.matrix_local ) ).inverted() -- cgit v1.2.3 From c7eda7cb49f706040cb04048b24c6db57501889e Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 21:18:35 +0200 Subject: glTF importer: add option to glue pieces of a mesh together Thanks scurest! --- io_scene_gltf2/__init__.py | 15 +++- io_scene_gltf2/blender/imp/gltf2_blender_mesh.py | 97 ++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 57bdf3d5..ba7b3848 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 39), + "version": (1, 3, 40), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -862,6 +862,18 @@ class ImportGLTF2(Operator, ImportHelper): default=True ) + merge_vertices: BoolProperty( + name='Merge Vertices', + description=( + 'The glTF format requires discontinuous normals, UVs, and ' + 'other vertex attributes to be stored as separate vertices, ' + 'as required for rendering on typical graphics hardware.\n' + 'This option attempts to combine co-located vertices where possible.\n' + 'Currently cannot combine verts with different normals' + ), + default=False, + ) + import_shading: EnumProperty( name="Shading", items=(("NORMALS", "Use Normal Data", ""), @@ -906,6 +918,7 @@ class ImportGLTF2(Operator, ImportHelper): layout.use_property_decorate = False # No animation. layout.prop(self, 'import_pack_images') + layout.prop(self, 'merge_vertices') layout.prop(self, 'import_shading') layout.prop(self, 'guess_original_bind_pose') layout.prop(self, 'bone_heuristic') diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py index 33578de8..175bc920 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -217,6 +217,14 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob): # the cache now that all prims are done. gltf.decode_accessor_cache = {} + if gltf.import_settings['merge_vertices']: + vert_locs, vert_normals, vert_joints, vert_weights, \ + sk_vert_locs, loop_vidxs, edge_vidxs = \ + merge_duplicate_verts( + vert_locs, vert_normals, vert_joints, vert_weights, \ + sk_vert_locs, loop_vidxs, edge_vidxs\ + ) + # --------------- # Convert all the arrays glTF -> Blender @@ -537,3 +545,92 @@ def skin_into_bind_pose(gltf, skin_idx, vert_joints, vert_weights, locs, vert_no def mul_mats_vecs(mats, vecs): """Given [m1,m2,...] and [v1,v2,...], returns [m1@v1,m2@v2,...]. 3D only.""" return np.matmul(mats, vecs.reshape(len(vecs), 3, 1)).reshape(len(vecs), 3) + + +def merge_duplicate_verts(vert_locs, vert_normals, vert_joints, vert_weights, sk_vert_locs, loop_vidxs, edge_vidxs): + # This function attempts to invert the splitting done when exporting to + # glTF. Welds together verts with the same per-vert data (but possibly + # different per-loop data). + # + # Ideally normals would be treated as per-loop data, but that has problems, + # so we currently treat the normal as per-vert. + # + # Strategy is simple: put all the per-vert data into an array of structs + # ("dots"), dedupe with np.unique, then take all the data back out. + + # Very often two verts that "morally" should be merged will have normals + # with very small differences. Round off the normals to smooth this over. + if len(vert_normals) != 0: + vert_normals *= 50000 + vert_normals[:] = np.trunc(vert_normals) + vert_normals *= (1/50000) + + dot_fields = [('x', np.float32), ('y', np.float32), ('z', np.float32)] + if len(vert_normals) != 0: + dot_fields += [('nx', np.float32), ('ny', np.float32), ('nz', np.float32)] + for i, _ in enumerate(vert_joints): + dot_fields += [ + ('joint%dx' % i, np.uint32), ('joint%dy' % i, np.uint32), + ('joint%dz' % i, np.uint32), ('joint%dw' % i, np.uint32), + ('weight%dx' % i, np.float32), ('weight%dy' % i, np.float32), + ('weight%dz' % i, np.float32), ('weight%dw' % i, np.float32), + ] + for i, _ in enumerate(sk_vert_locs): + dot_fields += [ + ('sk%dx' % i, np.float32), ('sk%dy' % i, np.float32), ('sk%dz' % i, np.float32), + ] + dots = np.empty(len(vert_locs), dtype=np.dtype(dot_fields)) + + dots['x'] = vert_locs[:, 0] + dots['y'] = vert_locs[:, 1] + dots['z'] = vert_locs[:, 2] + if len(vert_normals) != 0: + dots['nx'] = vert_normals[:, 0] + dots['ny'] = vert_normals[:, 1] + dots['nz'] = vert_normals[:, 2] + for i, (joints, weights) in enumerate(zip(vert_joints, vert_weights)): + dots['joint%dx' % i] = joints[:, 0] + dots['joint%dy' % i] = joints[:, 1] + dots['joint%dz' % i] = joints[:, 2] + dots['joint%dw' % i] = joints[:, 3] + dots['weight%dx' % i] = weights[:, 0] + dots['weight%dy' % i] = weights[:, 1] + dots['weight%dz' % i] = weights[:, 2] + dots['weight%dw' % i] = weights[:, 3] + for i, locs in enumerate(sk_vert_locs): + dots['sk%dx' % i] = locs[:, 0] + dots['sk%dy' % i] = locs[:, 1] + dots['sk%dz' % i] = locs[:, 2] + + unique_dots, inv_indices = np.unique(dots, return_inverse=True) + + loop_vidxs = inv_indices[loop_vidxs] + edge_vidxs = inv_indices[edge_vidxs] + + vert_locs = np.empty((len(unique_dots), 3), dtype=np.float32) + vert_locs[:, 0] = unique_dots['x'] + vert_locs[:, 1] = unique_dots['y'] + vert_locs[:, 2] = unique_dots['z'] + if len(vert_normals) != 0: + vert_normals = np.empty((len(unique_dots), 3), dtype=np.float32) + vert_normals[:, 0] = unique_dots['nx'] + vert_normals[:, 1] = unique_dots['ny'] + vert_normals[:, 2] = unique_dots['nz'] + for i in range(len(vert_joints)): + vert_joints[i] = np.empty((len(unique_dots), 4), dtype=np.uint32) + vert_joints[i][:, 0] = unique_dots['joint%dx' % i] + vert_joints[i][:, 1] = unique_dots['joint%dy' % i] + vert_joints[i][:, 2] = unique_dots['joint%dz' % i] + vert_joints[i][:, 3] = unique_dots['joint%dw' % i] + vert_weights[i] = np.empty((len(unique_dots), 4), dtype=np.float32) + vert_weights[i][:, 0] = unique_dots['weight%dx' % i] + vert_weights[i][:, 1] = unique_dots['weight%dy' % i] + vert_weights[i][:, 2] = unique_dots['weight%dz' % i] + vert_weights[i][:, 3] = unique_dots['weight%dw' % i] + for i in range(len(sk_vert_locs)): + sk_vert_locs[i] = np.empty((len(unique_dots), 3), dtype=np.float32) + sk_vert_locs[i][:, 0] = unique_dots['sk%dx' % i] + sk_vert_locs[i][:, 1] = unique_dots['sk%dy' % i] + sk_vert_locs[i][:, 2] = unique_dots['sk%dz' % i] + + return vert_locs, vert_normals, vert_joints, vert_weights, sk_vert_locs, loop_vidxs, edge_vidxs -- cgit v1.2.3 From 52f88967a6e7175cae857462eea90edf98e9ec5c Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 21:20:06 +0200 Subject: glTF exporter: always export loop normals Thanks scurest! --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_extract.py | 35 +++------------------- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index ba7b3848..a275ca95 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 40), + "version": (1, 3, 41), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index 6389d9dc..c605a609 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -136,9 +136,7 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert use_normals = export_settings[gltf2_blender_export_keys.NORMALS] if use_normals: - if blender_mesh.has_custom_normals: - # Custom normals are all (0, 0, 0) until calling calc_normals_split() or calc_tangents(). - blender_mesh.calc_normals_split() + blender_mesh.calc_normals_split() use_tangents = False if use_normals and export_settings[gltf2_blender_export_keys.TANGENTS]: @@ -236,21 +234,6 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert prim = Prim() prims[material_idx] = prim - if use_normals: - face_normal = None - if not (blender_polygon.use_smooth or blender_mesh.use_auto_smooth): - # Calc face normal/tangents - face_normal = blender_polygon.normal - if use_tangents: - face_tangent = Vector((0.0, 0.0, 0.0)) - face_bitangent = Vector((0.0, 0.0, 0.0)) - for loop_index in blender_polygon.loop_indices: - loop = blender_mesh.loops[loop_index] - face_tangent += loop.tangent - face_bitangent += loop.bitangent - face_tangent.normalize() - face_bitangent.normalize() - for loop_index in loop_tri.loops: vertex_index = blender_mesh.loops[loop_index].vertex_index vertex = blender_mesh.vertices[vertex_index] @@ -263,21 +246,11 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert vert += ((v[0], v[1], v[2]),) if use_normals: - if face_normal is None: - if blender_mesh.has_custom_normals: - n = blender_mesh.loops[loop_index].normal - else: - n = vertex.normal - if use_tangents: - t = blender_mesh.loops[loop_index].tangent - b = blender_mesh.loops[loop_index].bitangent - else: - n = face_normal - if use_tangents: - t = face_tangent - b = face_bitangent + n = blender_mesh.loops[loop_index].normal vert += ((n[0], n[1], n[2]),) if use_tangents: + t = blender_mesh.loops[loop_index].tangent + b = blender_mesh.loops[loop_index].bitangent vert += ((t[0], t[1], t[2]),) vert += ((b[0], b[1], b[2]),) # TODO: store just bitangent_sign in vert, not whole bitangent? -- cgit v1.2.3 From dbb4c80f22f7004c4f72e22b3a47e69f3cd782d2 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 21 Jul 2020 21:24:54 +0200 Subject: glTF: adding Scurest in contributor list --- io_scene_gltf2/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index a275ca95..85fdde3c 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -14,8 +14,8 @@ bl_info = { 'name': 'glTF 2.0 format', - 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 41), + 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', + "version": (1, 3, 42), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', -- cgit v1.2.3 From 85173fa5263a34924154f26c0c457dd5e79621ea Mon Sep 17 00:00:00 2001 From: Mikhail Rachinskiy Date: Wed, 22 Jul 2020 07:28:43 +0400 Subject: PLY: binary export Thanks to Adrian Vogelsgesang (@vogelsgesang) and his binary ply export implementation proposal D4252. I did not reuse any code from his patch, but it gave me a good starting point as I had no idea how to work with binary data. In this commit: * Implement export to binary little-endian file format. * Remove information about blend filename from exported file, it has no purpose. * Binary is the default format from now on. I cannot see any reason to implement big-endian option, if there is, please let me know. Below you will find performance comparison between ASCII and binary formats. Test geometry: * Verts 379 000 * Faces 378 000 Export: * ASCII (old) 8.0 sec * ASCII 3.0 sec * Binary 2.4 sec Note: difference between old and new ASCII export is due to avoiding unnecessary normal claculation when export normals is disabled. Import: ASCII 5.75 sec Binary 4.9 sec File sizes: * ASCII 20.6 MB * Binary 10.4 MB --- io_mesh_ply/__init__.py | 24 ++++++-- io_mesh_ply/export_ply.py | 154 ++++++++++++++++++++++++++++------------------ io_mesh_ply/import_ply.py | 16 ++--- 3 files changed, 120 insertions(+), 74 deletions(-) diff --git a/io_mesh_ply/__init__.py b/io_mesh_ply/__init__.py index 835ae60e..2cfb09c3 100644 --- a/io_mesh_ply/__init__.py +++ b/io_mesh_ply/__init__.py @@ -21,9 +21,9 @@ bl_info = { "name": "Stanford PLY format", "author": "Bruce Merry, Campbell Barton", - "version": (1, 1, 0), - "blender": (2, 82, 0), - "location": "File > Import-Export", + "version": (2, 0, 0), + "blender": (2, 90, 0), + "location": "File > Import/Export", "description": "Import-Export PLY mesh data with UVs and vertex colors", "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/mesh_ply.html", "support": 'OFFICIAL', @@ -107,6 +107,10 @@ class ExportPLY(bpy.types.Operator, ExportHelper): filename_ext = ".ply" filter_glob: StringProperty(default="*.ply", options={'HIDDEN'}) + use_ascii: BoolProperty( + name="ASCII", + description="Export using ASCII file format, otherwise use binary", + ) use_selection: BoolProperty( name="Selection Only", description="Export selected objects only", @@ -164,10 +168,20 @@ class ExportPLY(bpy.types.Operator, ExportHelper): filepath = self.filepath filepath = bpy.path.ensure_ext(filepath, self.filename_ext) - return export_ply.save(self, context, **keywords) + export_ply.save(context, **keywords) + + return {'FINISHED'} def draw(self, context): - pass + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + sfile = context.space_data + operator = sfile.active_operator + + col = layout.column(heading="Format") + col.prop(operator, "use_ascii") class PLY_PT_export_include(bpy.types.Panel): diff --git a/io_mesh_ply/export_ply.py b/io_mesh_ply/export_ply.py index 812aeb54..060b3d02 100644 --- a/io_mesh_ply/export_ply.py +++ b/io_mesh_ply/export_ply.py @@ -24,8 +24,55 @@ colors, and texture coordinates per face or per vertex. """ -def save_mesh(filepath, mesh, use_normals=True, use_uv_coords=True, use_colors=True): - import os +def _write_binary(fw, ply_verts, ply_faces, mesh_verts): + from struct import pack + + # Vertex data + # --------------------------- + + for index, normal, uv_coords, color in ply_verts: + fw(pack("<3f", *mesh_verts[index].co)) + if normal is not None: + fw(pack("<3f", *normal)) + if uv_coords is not None: + fw(pack("<2f", *uv_coords)) + if color is not None: + fw(pack("<4B", *color)) + + # Face data + # --------------------------- + + for pf in ply_faces: + length = len(pf) + fw(pack(" Date: Wed, 22 Jul 2020 09:53:31 +0400 Subject: PLY: flush selection on import --- io_mesh_ply/import_ply.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/io_mesh_ply/import_ply.py b/io_mesh_ply/import_ply.py index 2bc67746..c118aa3c 100644 --- a/io_mesh_ply/import_ply.py +++ b/io_mesh_ply/import_ply.py @@ -408,12 +408,16 @@ def load_ply(filepath): if not mesh: return {'CANCELLED'} + for ob in bpy.context.selected_objects: + ob.select_set(False) + obj = bpy.data.objects.new(ply_name, mesh) bpy.context.collection.objects.link(obj) bpy.context.view_layer.objects.active = obj obj.select_set(True) print("\nSuccessfully imported %r in %.3f sec" % (filepath, time.time() - t)) + return {'FINISHED'} -- cgit v1.2.3 From 711efc3e2c825e4a8dd683378c69b9750e08dade Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Wed, 22 Jul 2020 02:28:41 -0400 Subject: Collection Manager: Add Operator. Task: T69577 Adds a Remove Empty Collections operator in a new specials menu in the main Collection Manager popup. This operator has two modes: Mode one only removes collections if they don't have subcollections or objects. Mode two removes all collections that don't contain objects. Both of these modes are accessible via the new specials menu. --- object_collection_manager/__init__.py | 4 +- object_collection_manager/operator_utils.py | 82 +++++++++++++++++++++ object_collection_manager/operators.py | 106 +++++++++++++--------------- object_collection_manager/ui.py | 39 +++++++--- 4 files changed, 161 insertions(+), 70 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index f6a31695..90783e7e 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 9, 5), + "version": (2, 10, 0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -110,12 +110,14 @@ classes = ( operators.CMUnDisableRenderAllOperator, operators.CMNewCollectionOperator, operators.CMRemoveCollectionOperator, + operators.CMRemoveEmptyCollectionsOperator, operators.CMSetCollectionOperator, operators.CMPhantomModeOperator, preferences.CMPreferences, ui.CM_UL_items, ui.CollectionManager, ui.CMDisplayOptionsPanel, + ui.SpecialsMenu, CollectionManagerProperties, ) diff --git a/object_collection_manager/operator_utils.py b/object_collection_manager/operator_utils.py index f99d870b..d86a534f 100644 --- a/object_collection_manager/operator_utils.py +++ b/object_collection_manager/operator_utils.py @@ -17,12 +17,17 @@ # ##### END GPL LICENSE BLOCK ##### # Copyright 2011, Ryan Inch +import bpy from .internals import ( layer_collections, + qcd_slots, + expanded, + expand_history, rto_history, copy_buffer, swap_buffer, + update_property_group, ) rto_path = { @@ -289,3 +294,80 @@ def clear_swap(rto): swap_buffer["A"]["values"].clear() swap_buffer["B"]["RTO"] = "" swap_buffer["B"]["values"].clear() + + +def link_child_collections_to_parent(laycol, collection, parent_collection): + # store view layer RTOs for all children of the to be deleted collection + child_states = {} + def get_child_states(layer_collection): + child_states[layer_collection.name] = (layer_collection.exclude, + layer_collection.hide_viewport, + layer_collection.holdout, + layer_collection.indirect_only) + + apply_to_children(laycol["ptr"], get_child_states) + + # link any subcollections of the to be deleted collection to it's parent + for subcollection in collection.children: + if not subcollection.name in parent_collection.children: + parent_collection.children.link(subcollection) + + # apply the stored view layer RTOs to the newly linked collections and their + # children + def restore_child_states(layer_collection): + state = child_states.get(layer_collection.name) + + if state: + layer_collection.exclude = state[0] + layer_collection.hide_viewport = state[1] + layer_collection.holdout = state[2] + layer_collection.indirect_only = state[3] + + apply_to_children(laycol["parent"]["ptr"], restore_child_states) + + +def remove_collection(laycol, collection, context): + # get selected row + cm = context.scene.collection_manager + selected_row_name = cm.cm_list_collection[cm.cm_list_index].name + + # delete collection + bpy.data.collections.remove(collection) + + # update references + expanded.discard(laycol["name"]) + + if expand_history["target"] == laycol["name"]: + expand_history["target"] = "" + + if laycol["name"] in expand_history["history"]: + expand_history["history"].remove(laycol["name"]) + + if qcd_slots.contains(name=laycol["name"]): + qcd_slots.del_slot(name=laycol["name"]) + + if laycol["name"] in qcd_slots.overrides: + qcd_slots.overrides.remove(laycol["name"]) + + # reset history + for rto in rto_history.values(): + rto.clear() + + # update tree view + update_property_group(context) + + # update selected row + laycol = layer_collections.get(selected_row_name, None) + if laycol: + cm.cm_list_index = laycol["row_index"] + + elif len(cm.cm_list_collection) <= cm.cm_list_index: + cm.cm_list_index = len(cm.cm_list_collection) - 1 + + if cm.cm_list_index > -1: + name = cm.cm_list_collection[cm.cm_list_index].name + laycol = layer_collections[name] + while not laycol["visible"]: + laycol = laycol["parent"] + + cm.cm_list_index = laycol["row_index"] diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 77882c97..642860fa 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -63,6 +63,8 @@ from .operator_utils import ( swap_rtos, clear_copy, clear_swap, + link_child_collections_to_parent, + remove_collection, ) class SetActiveCollection(Operator): @@ -869,12 +871,9 @@ class CMRemoveCollectionOperator(Operator): global expand_history global qcd_slots - cm = context.scene.collection_manager - laycol = layer_collections[self.collection_name] collection = laycol["ptr"].collection parent_collection = laycol["parent"]["ptr"].collection - selected_row_name = cm.cm_list_collection[cm.cm_list_index].name # shift all objects in this collection to the parent collection @@ -885,78 +884,69 @@ class CMRemoveCollectionOperator(Operator): # shift all child collections to the parent collection preserving view layer RTOs if collection.children: - # store view layer RTOs for all children of the to be deleted collection - child_states = {} - def get_child_states(layer_collection): - child_states[layer_collection.name] = (layer_collection.exclude, - layer_collection.hide_viewport, - layer_collection.holdout, - layer_collection.indirect_only) - - apply_to_children(laycol["ptr"], get_child_states) - - # link any subcollections of the to be deleted collection to it's parent - for subcollection in collection.children: - if not subcollection.name in parent_collection.children: - parent_collection.children.link(subcollection) + link_child_collections_to_parent(laycol, collection, parent_collection) - # apply the stored view layer RTOs to the newly linked collections and their - # children - def restore_child_states(layer_collection): - state = child_states.get(layer_collection.name) - - if state: - layer_collection.exclude = state[0] - layer_collection.hide_viewport = state[1] - layer_collection.holdout = state[2] - layer_collection.indirect_only = state[3] - - apply_to_children(laycol["parent"]["ptr"], restore_child_states) + # remove collection, update references, and update tree view + remove_collection(laycol, collection, context) + return {'FINISHED'} - # remove collection, update expanded, and update tree view - bpy.data.collections.remove(collection) - expanded.discard(self.collection_name) - if expand_history["target"] == self.collection_name: - expand_history["target"] = "" - - if self.collection_name in expand_history["history"]: - expand_history["history"].remove(self.collection_name) +class CMRemoveEmptyCollectionsOperator(Operator): + bl_label = "Remove Empty Collections" + bl_idname = "view3d.remove_empty_collections" + bl_options = {'UNDO'} - update_property_group(context) + without_objects: BoolProperty() + @classmethod + def description(cls, context, properties): + if properties.without_objects: + tooltip = ( + "Purge All Collections Without Objects.\n" + "Deletes all collections that don't contain objects even if they have subcollections" + ) - # update selected row - laycol = layer_collections.get(selected_row_name, None) - if laycol: - cm.cm_list_index = laycol["row_index"] + else: + tooltip = ( + "Remove Empty Collections.\n" + "Delete collections that don't have any subcollections or objects" + ) - elif len(cm.cm_list_collection) == cm.cm_list_index: - cm.cm_list_index -= 1 + return tooltip - if cm.cm_list_index > -1: - name = cm.cm_list_collection[cm.cm_list_index].name - laycol = layer_collections[name] - while not laycol["visible"]: - laycol = laycol["parent"] + def execute(self, context): + global rto_history + global expand_history + global qcd_slots - cm.cm_list_index = laycol["row_index"] + if self.without_objects: + empty_collections = [laycol["name"] + for laycol in layer_collections.values() + if not laycol["ptr"].collection.objects] + else: + empty_collections = [laycol["name"] + for laycol in layer_collections.values() + if not laycol["children"] and + not laycol["ptr"].collection.objects] + for name in empty_collections: + laycol = layer_collections[name] + collection = laycol["ptr"].collection + parent_collection = laycol["parent"]["ptr"].collection - # update qcd - if qcd_slots.contains(name=self.collection_name): - qcd_slots.del_slot(name=self.collection_name) + # link all child collections to the parent collection preserving view layer RTOs + if collection.children: + link_child_collections_to_parent(laycol, collection, parent_collection) - if self.collection_name in qcd_slots.overrides: - qcd_slots.overrides.remove(self.collection_name) + # remove collection, update references, and update tree view + remove_collection(laycol, collection, context) - # reset history - for rto in rto_history.values(): - rto.clear() + self.report({"INFO"}, f"Removed {len(empty_collections)} collections") return {'FINISHED'} + rename = [False] class CMNewCollectionOperator(Operator): bl_label = "Add New Collection" diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 838c5d18..fab65c67 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -21,6 +21,7 @@ import bpy from bpy.types import ( + Menu, Operator, Panel, UIList, @@ -112,9 +113,9 @@ class CollectionManager(Operator): layout.row().separator() # buttons - button_row = layout.row() + button_row_1 = layout.row() - op_sec = button_row.row() + op_sec = button_row_1.row() op_sec.alignment = 'LEFT' collapse_sec = op_sec.row() @@ -138,11 +139,12 @@ class CollectionManager(Operator): renum_sec.alignment = 'LEFT' renum_sec.operator("view3d.renumerate_qcd_slots") - # filter - filter_sec = button_row.row() - filter_sec.alignment = 'RIGHT' + # menu & filter + right_sec = button_row_1.row() + right_sec.alignment = 'RIGHT' - filter_sec.popover(panel="COLLECTIONMANAGER_PT_display_options", + right_sec.menu("VIEW3D_MT_CM_specials_menu") + right_sec.popover(panel="COLLECTIONMANAGER_PT_display_options", text="", icon='FILTER') mc_box = layout.box() @@ -304,19 +306,19 @@ class CollectionManager(Operator): sort_lock=True) # add collections - addcollec_row = layout.row() - prop = addcollec_row.operator("view3d.add_collection", text="Add Collection", + button_row_2 = layout.row() + prop = button_row_2.operator("view3d.add_collection", text="Add Collection", icon='COLLECTION_NEW') prop.child = False - prop = addcollec_row.operator("view3d.add_collection", text="Add SubCollection", + prop = button_row_2.operator("view3d.add_collection", text="Add SubCollection", icon='COLLECTION_NEW') prop.child = True # phantom mode - phantom_row = layout.row() + button_row_3 = layout.row() toggle_text = "Disable " if cm.in_phantom_mode else "Enable " - phantom_row.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode") + button_row_3.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode") if cm.in_phantom_mode: view.enabled = False @@ -748,6 +750,21 @@ class CMDisplayOptionsPanel(Panel): row.prop(cm, "align_local_ops") +class SpecialsMenu(Menu): + bl_label = "Specials" + bl_idname = "VIEW3D_MT_CM_specials_menu" + + def draw(self, context): + layout = self.layout + + prop = layout.operator("view3d.remove_empty_collections") + prop.without_objects = False + + prop = layout.operator("view3d.remove_empty_collections", + text="Purge All Collections Without Objects") + prop.without_objects = True + + def view3d_header_qcd_slots(self, context): layout = self.layout -- cgit v1.2.3 From 09133c5abdc1236ea61a98e68a0f23ba3503b472 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Wed, 22 Jul 2020 02:44:42 -0400 Subject: Collection Manager: Update QCD Renumbering. Task: T69577 Added a linear renumbering option. Added a constrain to branch option. Allowed all options to be combined with each other. Updated tooltip. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/internals.py | 55 +++++++++++++++++++----------- object_collection_manager/qcd_operators.py | 27 ++++++++++----- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 90783e7e..04422b84 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 10, 0), + "version": (2, 11, 0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py index 8e0c5b90..8a225443 100644 --- a/object_collection_manager/internals.py +++ b/object_collection_manager/internals.py @@ -210,46 +210,63 @@ class QCDSlots(): if self.length() > 20: break - def renumerate(self, *, depth_first=False, beginning=False): + def renumerate(self, *, beginning=False, depth_first=False, constrain=False): if beginning: self.clear_slots() self.overrides.clear() starting_laycol_name = self.get_name("1") - if starting_laycol_name: - laycol = layer_collections[starting_laycol_name]["parent"]["ptr"] - else: + if not starting_laycol_name: laycol = bpy.context.view_layer.layer_collection starting_laycol_name = laycol.children[0].name self.clear_slots() self.overrides.clear() - laycol_iter_list = [] - for laycol in laycol.children: - if laycol.name == starting_laycol_name or laycol_iter_list: - laycol_iter_list.append(laycol) + if depth_first: + parent = layer_collections[starting_laycol_name]["parent"] + x = 1 + + for laycol in layer_collections.values(): + if self.length() == 0 and starting_laycol_name != laycol["name"]: + continue + + if constrain: + if self.length(): + if laycol["parent"]["name"] == parent["name"]: + break - while laycol_iter_list: - layer_collection = laycol_iter_list.pop(0) + self.add_slot(f"{x}", laycol["name"]) - for x in range(20): - if self.contains(name=layer_collection.name): + x += 1 + + if self.length() > 20: break - if not self.contains(idx=f"{x+1}"): - self.add_slot(f"{x+1}", layer_collection.name) + else: + laycol = layer_collections[starting_laycol_name]["parent"]["ptr"] + laycol_iter_list = [] + for laycol in laycol.children: + if laycol.name == starting_laycol_name: + laycol_iter_list.append(laycol) - if depth_first: - laycol_iter_list[0:0] = list(layer_collection.children) + elif not constrain and laycol_iter_list: + laycol_iter_list.append(laycol) + + x = 1 + while laycol_iter_list: + layer_collection = laycol_iter_list.pop(0) + + self.add_slot(f"{x}", layer_collection.name) - else: laycol_iter_list.extend(list(layer_collection.children)) - if self.length() > 20: - break + x += 1 + + if self.length() > 20: + break for laycol in layer_collections.values(): diff --git a/object_collection_manager/qcd_operators.py b/object_collection_manager/qcd_operators.py index 7330dd0f..56df1501 100644 --- a/object_collection_manager/qcd_operators.py +++ b/object_collection_manager/qcd_operators.py @@ -287,9 +287,10 @@ class RenumerateQCDSlots(Operator): bl_label = "Renumber QCD Slots" bl_description = ( "Renumber QCD slots.\n" - " * LMB - Renumber (breadth first) starting from the slot designated 1.\n" - " * Ctrl+LMB - Renumber (depth first) starting from the slot designated 1.\n" - " * Alt+LMB - Renumber from the beginning" + " * LMB - Renumber (breadth first) from slot 1.\n" + " * +Ctrl - Linear.\n" + " * +Alt - Reset.\n" + " * +Shift - Constrain to branch" ) bl_idname = "view3d.renumerate_qcd_slots" bl_options = {'REGISTER', 'UNDO'} @@ -299,14 +300,22 @@ class RenumerateQCDSlots(Operator): modifiers = get_modifiers(event) - if modifiers == {'alt'}: - qcd_slots.renumerate(beginning=True) + beginning = False + depth_first = False + constrain = False - elif modifiers == {'ctrl'}: - qcd_slots.renumerate(depth_first=True) + if 'alt' in modifiers: + beginning=True - else: - qcd_slots.renumerate() + if 'ctrl' in modifiers: + depth_first=True + + if 'shift' in modifiers: + constrain=True + + qcd_slots.renumerate(beginning=beginning, + depth_first=depth_first, + constrain=constrain) update_property_group(context) -- cgit v1.2.3 From cee175131c6d71a5c7043c8c3e177654d7b7ad86 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Wed, 22 Jul 2020 04:05:26 -0400 Subject: Collection Manager: Add Operator. Task: T69577 Add Apply Phantom Mode operator. --- object_collection_manager/__init__.py | 3 ++- object_collection_manager/operators.py | 12 ++++++++++++ object_collection_manager/ui.py | 8 ++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 04422b84..fb9c0c49 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 11, 0), + "version": (2, 12, 0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -113,6 +113,7 @@ classes = ( operators.CMRemoveEmptyCollectionsOperator, operators.CMSetCollectionOperator, operators.CMPhantomModeOperator, + operators.CMApplyPhantomModeOperator, preferences.CMPreferences, ui.CM_UL_items, ui.CollectionManager, diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 642860fa..1025535e 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -1099,3 +1099,15 @@ class CMPhantomModeOperator(Operator): return {'FINISHED'} + + +class CMApplyPhantomModeOperator(Operator): + '''Make all changes made in Phantom Mode permanent''' + bl_label = "Apply Phantom Mode" + bl_idname = "view3d.apply_phantom_mode" + + def execute(self, context): + cm = context.scene.collection_manager + cm.in_phantom_mode = False + + return {'FINISHED'} diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index fab65c67..c6403fe2 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -315,10 +315,14 @@ class CollectionManager(Operator): icon='COLLECTION_NEW') prop.child = True - # phantom mode + button_row_3 = layout.row() + + # phantom mode + phantom_mode = button_row_3.row(align=True) toggle_text = "Disable " if cm.in_phantom_mode else "Enable " - button_row_3.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode") + phantom_mode.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode") + phantom_mode.operator("view3d.apply_phantom_mode", text="", icon='CHECKMARK') if cm.in_phantom_mode: view.enabled = False -- cgit v1.2.3 From 294f97f0330ad3204c02b6f05a8cb2f5ea71f027 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Wed, 22 Jul 2020 04:12:32 -0400 Subject: Collection Manager: Small improvement. Task:T69577 Prevent the 'Expander' operators from being added to the undo stack because they can't be undone properly and only add clutter. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/operators.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index fb9c0c49..7fe71187 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 0), + "version": (2, 12, 1), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 1025535e..177ab3d7 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -100,7 +100,6 @@ class ExpandAllOperator(Operator): '''Expand/Collapse all collections''' bl_label = "Expand All Items" bl_idname = "view3d.expand_all_items" - bl_options = {'REGISTER', 'UNDO'} def execute(self, context): global expand_history @@ -131,7 +130,6 @@ class ExpandSublevelOperator(Operator): " * Alt+LMB - Discard history" ) bl_idname = "view3d.expand_sublevel" - bl_options = {'REGISTER', 'UNDO'} expand: BoolProperty() name: StringProperty() -- cgit v1.2.3 From fe21f93ae98d7633c3caf93a68caa1d5ebf06c54 Mon Sep 17 00:00:00 2001 From: Sebastian Koenig Date: Wed, 22 Jul 2020 12:03:26 +0200 Subject: VR Scene-Inspection: Extend Landmarks feature set * Enable custom poses for landmarks (so they don't require adding a new camera). * New landmark operators, available in Sidebar menu: ** "Add VR Landmark from Selected Camera" ** "Update Custom Landmark" (updates landmark to match current VR viewer pose) ** "Cursor to VR Landmark" ** "Active Camera to Landmark" ** "New Camera from Landmark" * "Show Landmarks" option, adding gizmos as landmark indicators to 3D Views. This should make the landmarks more practical. Patch by Sebastian Koenig, with some smaller edits. Followup commits will do further edits. Part of T71347. --- viewport_vr_preview.py | 304 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 281 insertions(+), 23 deletions(-) diff --git a/viewport_vr_preview.py b/viewport_vr_preview.py index db8dd176..11437fe1 100644 --- a/viewport_vr_preview.py +++ b/viewport_vr_preview.py @@ -22,6 +22,11 @@ import bpy from bpy.types import ( Gizmo, GizmoGroup, + PropertyGroup, + UIList, + Menu, + Panel, + Operator, ) from bpy.props import ( CollectionProperty, @@ -32,7 +37,7 @@ from bpy.app.handlers import persistent bl_info = { "name": "VR Scene Inspection", - "author": "Julian Eisel (Severin)", + "author": "Julian Eisel (Severin), Sebastian Koenig", "version": (0, 2, 0), "blender": (2, 83, 0), "location": "3D View > Sidebar > VR", @@ -65,8 +70,8 @@ def xr_landmark_active_type_update(self, context): session_settings.base_pose_type = 'SCENE_CAMERA' elif landmark_active.type == 'USER_CAMERA': session_settings.base_pose_type = 'OBJECT' - # elif landmark_active.type == 'CUSTOM': - # session_settings.base_pose_type = 'CUSTOM' + elif landmark_active.type == 'CUSTOM': + session_settings.base_pose_type = 'CUSTOM' def xr_landmark_active_camera_update(self, context): @@ -147,7 +152,21 @@ def xr_landmark_active_update(self, context): wm.xr_session_state.reset_to_base_pose(context) -class VRLandmark(bpy.types.PropertyGroup): +class VIEW3D_MT_landmark_menu(Menu): + bl_label = "Landmark Controls" + + def draw(self, _context): + layout = self.layout + + layout.operator("view3d.vr_landmark_from_camera") + layout.operator("view3d.update_vr_landmark") + layout.separator() + layout.operator("view3d.cursor_to_vr_landmark") + layout.operator("view3d.active_cam_to_vr_landmark") + layout.operator("view3d.new_cam_to_vr_landmark") + + +class VRLandmark(PropertyGroup): name: bpy.props.StringProperty( name="VR Landmark", default="Landmark" @@ -161,11 +180,9 @@ class VRLandmark(bpy.types.PropertyGroup): ('USER_CAMERA', "Custom Camera", "Use an existing camera to define the VR view base location and " "rotation"), - # Custom base poses work, but it's uncertain if they are really - # needed. Disabled for now. - # ('CUSTOM', "Custom Pose", - # "Allow a manually definied position and rotation to be used as " - # "the VR view base pose"), + ('CUSTOM', "Custom Pose", + "Allow a manually definied position and rotation to be used as " + "the VR view base pose"), ], default='SCENE_CAMERA', update=xr_landmark_type_update, @@ -209,7 +226,7 @@ class VRLandmark(bpy.types.PropertyGroup): ) -class VIEW3D_UL_vr_landmarks(bpy.types.UIList): +class VIEW3D_UL_vr_landmarks(UIList): def draw_item(self, context, layout, _data, item, icon, _active_data, _active_propname, index): landmark = item @@ -227,7 +244,7 @@ class VIEW3D_UL_vr_landmarks(bpy.types.UIList): props.index = index -class VIEW3D_PT_vr_landmarks(bpy.types.Panel): +class VIEW3D_PT_vr_landmarks(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" @@ -250,20 +267,23 @@ class VIEW3D_PT_vr_landmarks(bpy.types.Panel): col = row.column(align=True) col.operator("view3d.vr_landmark_add", icon='ADD', text="") col.operator("view3d.vr_landmark_remove", icon='REMOVE', text="") + col.operator("view3d.vr_landmark_from_session", icon='PLUS', text="") + + col.menu("VIEW3D_MT_landmark_menu", icon='DOWNARROW_HLT', text="") if landmark_selected: layout.prop(landmark_selected, "type") if landmark_selected.type == 'USER_CAMERA': layout.prop(landmark_selected, "base_pose_camera") - # elif landmark_selected.type == 'CUSTOM': - # layout.prop(landmark_selected, - # "base_pose_location", text="Location") - # layout.prop(landmark_selected, - # "base_pose_angle", text="Angle") + elif landmark_selected.type == 'CUSTOM': + layout.prop(landmark_selected, + "base_pose_location", text="Location") + layout.prop(landmark_selected, + "base_pose_angle", text="Angle") -class VIEW3D_PT_vr_session_view(bpy.types.Panel): +class VIEW3D_PT_vr_session_view(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" @@ -285,7 +305,7 @@ class VIEW3D_PT_vr_session_view(bpy.types.Panel): col.prop(session_settings, "clip_end", text="End") -class VIEW3D_PT_vr_session(bpy.types.Panel): +class VIEW3D_PT_vr_session(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" @@ -295,7 +315,8 @@ class VIEW3D_PT_vr_session(bpy.types.Panel): layout = self.layout session_settings = context.window_manager.xr_session_settings - layout.use_property_split = False + layout.use_property_split = True + layout.use_property_decorate = False # No animation. is_session_running = bpy.types.XrSessionState.is_running(context) @@ -327,7 +348,8 @@ class VIEW3D_PT_vr_info(bpy.types.Panel): layout = self.layout layout.label(icon='ERROR', text="Built without VR/OpenXR features.") -class VIEW3D_OT_vr_landmark_add(bpy.types.Operator): + +class VIEW3D_OT_vr_landmark_add(Operator): bl_idname = "view3d.vr_landmark_add" bl_label = "Add VR Landmark" bl_description = "Add a new VR landmark to the list and select it" @@ -345,7 +367,104 @@ class VIEW3D_OT_vr_landmark_add(bpy.types.Operator): return {'FINISHED'} -class VIEW3D_OT_vr_landmark_remove(bpy.types.Operator): +class VIEW3D_OT_vr_landmark_from_camera(Operator): + bl_idname = "view3d.vr_landmark_from_camera" + bl_label = "Add VR Landmark from selected camera" + bl_description = "Add a new VR landmark from the selected camera to the list and select it" + bl_options = {'UNDO', 'REGISTER'} + + @classmethod + def poll(cls, context): + cam_selected = 0 + + vl_objects = bpy.context.view_layer.objects + if vl_objects.active and vl_objects.active.type == 'CAMERA': + cam_selected = 1 + return cam_selected + + def execute(self, context): + scene = context.scene + landmarks = scene.vr_landmarks + cam = context.view_layer.objects.active + lm = landmarks.add() + lm.type = 'USER_CAMERA' + lm.base_pose_camera = cam + lm.name = "LM_" + cam.name + + # select newly created set + scene.vr_landmarks_selected = len(landmarks) - 1 + + return {'FINISHED'} + + +class VIEW3D_OT_vr_landmark_from_session(Operator): + bl_idname = "view3d.vr_landmark_from_session" + bl_label = "Add VR Landmark from session" + bl_description = "Add VR landmark from the current session to the list and select it" + bl_options = {'UNDO', 'REGISTER'} + + @classmethod + def poll(cls, context): + view3d = context.space_data + return bpy.types.XrSessionState.is_running(context) + + def execute(self, context): + from mathutils import Matrix, Quaternion + scene = context.scene + landmarks = scene.vr_landmarks + wm = context.window_manager + + lm = landmarks.add() + lm.type = "CUSTOM" + + loc = wm.xr_session_state.viewer_pose_location + rot = wm.xr_session_state.viewer_pose_rotation.to_euler() + + lm.base_pose_location = loc + lm.base_pose_angle = rot[2] + + return {'FINISHED'} + + +class VIEW3D_OT_update_vr_landmark(Operator): + bl_idname = "view3d.update_vr_landmark" + bl_label = "Update Custom Landmark" + bl_description = "Update an existing landmark from live session" + bl_options = {'UNDO', 'REGISTER'} + + @classmethod + def poll(cls, context): + view3d = context.space_data + scene = context.scene + landmarks = scene.vr_landmarks + active_landmark = scene.vr_landmarks[scene.vr_landmarks_active] + # return bpy.types.XrSessionState.is_running(context) and active_landmark.type == 'CUSTOM' + return active_landmark.type == 'CUSTOM' + + def execute(self, context): + from mathutils import Matrix, Quaternion + scene = context.scene + landmarks = scene.vr_landmarks + wm = context.window_manager + + lm = landmarks[scene.vr_landmarks_active] + + loc = wm.xr_session_state.viewer_pose_location + rot = wm.xr_session_state.viewer_pose_rotation.to_euler() + # only for testing + # loc = landmarks[0].base_pose_location + # rot = landmarks[0].base_pose_angle + + lm.base_pose_location = loc + lm.base_pose_angle = rot + + # now activate the landmark again to trigger viewer reset + bpy.ops.view3d.vr_landmark_activate() + + return {'FINISHED'} + + +class VIEW3D_OT_vr_landmark_remove(Operator): bl_idname = "view3d.vr_landmark_remove" bl_label = "Remove VR Landmark" bl_description = "Delete the selected VR landmark from the list" @@ -364,7 +483,71 @@ class VIEW3D_OT_vr_landmark_remove(bpy.types.Operator): return {'FINISHED'} -class VIEW3D_OT_vr_landmark_activate(bpy.types.Operator): +class VIEW3D_OT_cursor_to_vr_landmark(Operator): + bl_idname = "view3d.cursor_to_vr_landmark" + bl_label = "Cursor to VR Landmark" + bl_description = "Set the 3D Cursor to the active VR Landmark" + bl_options = {'UNDO', 'REGISTER'} + + def execute(self, context): + scene = context.scene + lm = scene.vr_landmarks[scene.vr_landmarks_selected] + if lm.type == 'SCENE_CAMERA': + lm_pos = scene.camera.location + elif lm.type == 'USER_CAMERA': + lm_pos = lm.base_pose_camera.location + else: + lm_pos = lm.base_pose_location + scene.cursor.location = lm_pos + + return{'FINISHED'} + + +class VIEW3d_OT_new_cam_to_vr_landmark(Operator): + bl_idname = "view3d.new_cam_to_vr_landmark" + bl_label = "New Camera from Landmark" + bl_description = "Create a new Camera from active VR Landmark" + bl_options = {'UNDO', 'REGISTER'} + + def execute(self, context): + scene = context.scene + + lm = scene.vr_landmarks[scene.vr_landmarks_selected] + + cam = bpy.data.cameras.new("Camera_" + lm.name) + new_cam = bpy.data.objects.new("Camera_" + lm.name, cam) + scene.collection.objects.link(new_cam) + angle = lm.base_pose_angle + new_cam.location = lm.base_pose_location + new_cam.rotation_euler = (1.5708, 0, angle) + + return {'FINISHED'} + + +class VIEW3D_OT_active_cam_to_vr_landmark(Operator): + bl_idname = "view3d.active_cam_to_vr_landmark" + bl_label = "Active Camera to Landmark" + bl_description = "Position the active camera at the selected landmark" + bl_options = {'UNDO', 'REGISTER'} + + @classmethod + def poll(cls, context): + return context.scene.camera is not None + + def execute(self, context): + scene = context.scene + + lm = scene.vr_landmarks[scene.vr_landmarks_selected] + + cam = scene.camera + angle = lm.base_pose_angle + cam.location = lm.base_pose_location + cam.rotation_euler = (1.5708, 0, angle) + + return {'FINISHED'} + + +class VIEW3D_OT_vr_landmark_activate(Operator): bl_idname = "view3d.vr_landmark_activate" bl_label = "Activate VR Landmark" bl_description = "Change to the selected VR landmark from the list" @@ -389,7 +572,7 @@ class VIEW3D_OT_vr_landmark_activate(bpy.types.Operator): return {'FINISHED'} -class VIEW3D_PT_vr_viewport_feedback(bpy.types.Panel): +class VIEW3D_PT_vr_viewport_feedback(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "VR" @@ -408,6 +591,7 @@ class VIEW3D_PT_vr_viewport_feedback(bpy.types.Panel): layout.separator() layout.prop(view3d.shading, "vr_show_virtual_camera") + layout.prop(view3d.shading, "vr_show_landmarks") layout.prop(view3d, "mirror_xr_session") @@ -497,6 +681,68 @@ class VIEW3D_GGT_vr_viewer_pose(GizmoGroup): self.gizmo.matrix_basis = self._get_viewer_pose_matrix(context) +class VIEW3D_GGT_vr_viewer_viz(GizmoGroup): + bl_idname = "VIEW3D_GGT_vr_viewer_viz" + bl_label = "VR Landmark Indicator" + bl_space_type = 'VIEW_3D' + bl_region_type = 'WINDOW' + bl_options = {'3D', 'PERSISTENT', 'SCALE'} + + @classmethod + def poll(cls, context): + view3d = context.space_data + return ( + view3d.shading.vr_show_landmarks + ) + + def setup(self,context): + pass + + def draw_prepare(self, context): + # first delete the old gizmos + for g in self.gizmos: + self.gizmos.remove(g) + + from math import radians + from mathutils import Matrix, Euler + landmarks = context.scene.vr_landmarks + + default_matrix = Matrix(((1.0, 0.0, 0.0, 0.0), + (0.0, 1.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, 0.0, 0.0, 1.0))) + + for lm in landmarks: + gizmo = self.gizmos.new(VIEW3D_GT_vr_camera_cone.bl_idname) + gizmo.aspect = 1 / 3, 1 / 4 + + gizmo.color = gizmo.color_highlight = 0.2, 1.0, 0.6 + gizmo.alpha = 1.0 + + self.gizmo = gizmo + + if lm.type == 'SCENE_CAMERA': + if context.scene.camera: + lm_mat = context.scene.camera.matrix_world + else: + lm_mat = default_matrix + elif lm.type == 'USER_CAMERA': + lm_mat = lm.base_pose_camera.matrix_world + else: + angle = lm.base_pose_angle + raw_rot = Euler((radians(90.0), 0, angle)) + + rotmat = Matrix.Identity(3) + rotmat.rotate(raw_rot) + rotmat.resize_4x4() + + transmat = Matrix.Translation(lm.base_pose_location) + + lm_mat = transmat @ rotmat + + self.gizmo.matrix_basis = lm_mat + + classes = ( VIEW3D_PT_vr_session, VIEW3D_PT_vr_session_view, @@ -505,13 +751,21 @@ classes = ( VRLandmark, VIEW3D_UL_vr_landmarks, + VIEW3D_MT_landmark_menu, VIEW3D_OT_vr_landmark_add, VIEW3D_OT_vr_landmark_remove, VIEW3D_OT_vr_landmark_activate, + VIEW3D_OT_vr_landmark_from_session, + VIEW3d_OT_new_cam_to_vr_landmark, + VIEW3D_OT_active_cam_to_vr_landmark, + VIEW3D_OT_vr_landmark_from_camera, + VIEW3D_OT_cursor_to_vr_landmark, + VIEW3D_OT_update_vr_landmark, VIEW3D_GT_vr_camera_cone, VIEW3D_GGT_vr_viewer_pose, + VIEW3D_GGT_vr_viewer_viz, ) @@ -538,6 +792,9 @@ def register(): bpy.types.View3DShading.vr_show_virtual_camera = BoolProperty( name="Show VR Camera" ) + bpy.types.View3DShading.vr_show_landmarks = BoolProperty( + name="Show Landmarks" + ) bpy.app.handlers.load_post.append(ensure_default_vr_landmark) @@ -554,6 +811,7 @@ def unregister(): del bpy.types.Scene.vr_landmarks_selected del bpy.types.Scene.vr_landmarks_active del bpy.types.View3DShading.vr_show_virtual_camera + del bpy.types.View3DShading.vr_show_landmarks bpy.app.handlers.load_post.remove(ensure_default_vr_landmark) -- cgit v1.2.3 From 362200bfeed6c5f52519030b6e8c60af1120f65c Mon Sep 17 00:00:00 2001 From: Julian Eisel Date: Wed, 22 Jul 2020 14:05:48 +0200 Subject: VR Scene Inspection: Various fixes and cleanups for preview changes Besides minor tweaks: * Always use selected, not active landmark for editing operators * Fix failure when trying to access non-existant scene camera or custom base pose camera * More consistent naming and descriptions --- viewport_vr_preview.py | 123 ++++++++++++++++++++++++------------------------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/viewport_vr_preview.py b/viewport_vr_preview.py index 11437fe1..7db9cea6 100644 --- a/viewport_vr_preview.py +++ b/viewport_vr_preview.py @@ -149,7 +149,7 @@ def xr_landmark_active_update(self, context): xr_landmark_active_base_pose_angle_update(self, context) if wm.xr_session_state: - wm.xr_session_state.reset_to_base_pose(context) + wm.xr_session_state.reset_to_base_pose(context) class VIEW3D_MT_landmark_menu(Menu): @@ -162,8 +162,8 @@ class VIEW3D_MT_landmark_menu(Menu): layout.operator("view3d.update_vr_landmark") layout.separator() layout.operator("view3d.cursor_to_vr_landmark") - layout.operator("view3d.active_cam_to_vr_landmark") - layout.operator("view3d.new_cam_to_vr_landmark") + layout.operator("view3d.camera_to_vr_landmark") + layout.operator("view3d.add_camera_from_vr_landmark") class VRLandmark(PropertyGroup): @@ -369,17 +369,17 @@ class VIEW3D_OT_vr_landmark_add(Operator): class VIEW3D_OT_vr_landmark_from_camera(Operator): bl_idname = "view3d.vr_landmark_from_camera" - bl_label = "Add VR Landmark from selected camera" - bl_description = "Add a new VR landmark from the selected camera to the list and select it" + bl_label = "Add VR Landmark from camera" + bl_description = "Add a new VR landmark from the active camera object to the list and select it" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): - cam_selected = 0 + cam_selected = False vl_objects = bpy.context.view_layer.objects if vl_objects.active and vl_objects.active.type == 'CAMERA': - cam_selected = 1 + cam_selected = True return cam_selected def execute(self, context): @@ -400,22 +400,21 @@ class VIEW3D_OT_vr_landmark_from_camera(Operator): class VIEW3D_OT_vr_landmark_from_session(Operator): bl_idname = "view3d.vr_landmark_from_session" bl_label = "Add VR Landmark from session" - bl_description = "Add VR landmark from the current session to the list and select it" + bl_description = "Add VR landmark from the viewer pose of the running VR session to the list and select it" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): - view3d = context.space_data return bpy.types.XrSessionState.is_running(context) def execute(self, context): - from mathutils import Matrix, Quaternion scene = context.scene landmarks = scene.vr_landmarks wm = context.window_manager lm = landmarks.add() lm.type = "CUSTOM" + scene.vr_landmarks_selected = len(landmarks) - 1 loc = wm.xr_session_state.viewer_pose_location rot = wm.xr_session_state.viewer_pose_rotation.to_euler() @@ -428,38 +427,28 @@ class VIEW3D_OT_vr_landmark_from_session(Operator): class VIEW3D_OT_update_vr_landmark(Operator): bl_idname = "view3d.update_vr_landmark" - bl_label = "Update Custom Landmark" - bl_description = "Update an existing landmark from live session" + bl_label = "Update Custom VR Landmark" + bl_description = "Update the selected landmark from the current viewer pose in the VR session" bl_options = {'UNDO', 'REGISTER'} @classmethod def poll(cls, context): - view3d = context.space_data - scene = context.scene - landmarks = scene.vr_landmarks - active_landmark = scene.vr_landmarks[scene.vr_landmarks_active] - # return bpy.types.XrSessionState.is_running(context) and active_landmark.type == 'CUSTOM' - return active_landmark.type == 'CUSTOM' + selected_landmark = VRLandmark.get_selected_landmark(context) + return bpy.types.XrSessionState.is_running(context) and selected_landmark.type == 'CUSTOM' def execute(self, context): - from mathutils import Matrix, Quaternion - scene = context.scene - landmarks = scene.vr_landmarks wm = context.window_manager - lm = landmarks[scene.vr_landmarks_active] + lm = VRLandmark.get_selected_landmark(context) loc = wm.xr_session_state.viewer_pose_location rot = wm.xr_session_state.viewer_pose_rotation.to_euler() - # only for testing - # loc = landmarks[0].base_pose_location - # rot = landmarks[0].base_pose_angle lm.base_pose_location = loc lm.base_pose_angle = rot - # now activate the landmark again to trigger viewer reset - bpy.ops.view3d.vr_landmark_activate() + # Re-activate the landmark to trigger viewer reset and flush landmark settings to the session settings. + xr_landmark_active_update(None, context) return {'FINISHED'} @@ -486,12 +475,22 @@ class VIEW3D_OT_vr_landmark_remove(Operator): class VIEW3D_OT_cursor_to_vr_landmark(Operator): bl_idname = "view3d.cursor_to_vr_landmark" bl_label = "Cursor to VR Landmark" - bl_description = "Set the 3D Cursor to the active VR Landmark" + bl_description = "Move the 3D Cursor to the selected VR Landmark" bl_options = {'UNDO', 'REGISTER'} + @classmethod + def poll(cls, context): + lm = VRLandmark.get_selected_landmark(context) + if lm.type == 'SCENE_CAMERA': + return context.scene.camera is not None + elif lm.type == 'USER_CAMERA': + return lm.base_pose_camera is not None + + return True + def execute(self, context): scene = context.scene - lm = scene.vr_landmarks[scene.vr_landmarks_selected] + lm = VRLandmark.get_selected_landmark(context) if lm.type == 'SCENE_CAMERA': lm_pos = scene.camera.location elif lm.type == 'USER_CAMERA': @@ -503,31 +502,32 @@ class VIEW3D_OT_cursor_to_vr_landmark(Operator): return{'FINISHED'} -class VIEW3d_OT_new_cam_to_vr_landmark(Operator): - bl_idname = "view3d.new_cam_to_vr_landmark" - bl_label = "New Camera from Landmark" - bl_description = "Create a new Camera from active VR Landmark" +class VIEW3d_OT_add_camera_from_vr_landmark(Operator): + bl_idname = "view3d.add_camera_from_vr_landmark" + bl_label = "New Camera from VR Landmark" + bl_description = "Create a new Camera from the selected VR Landmark" bl_options = {'UNDO', 'REGISTER'} def execute(self, context): - scene = context.scene + import math - lm = scene.vr_landmarks[scene.vr_landmarks_selected] + scene = context.scene + lm = VRLandmark.get_selected_landmark(context) cam = bpy.data.cameras.new("Camera_" + lm.name) new_cam = bpy.data.objects.new("Camera_" + lm.name, cam) scene.collection.objects.link(new_cam) angle = lm.base_pose_angle new_cam.location = lm.base_pose_location - new_cam.rotation_euler = (1.5708, 0, angle) + new_cam.rotation_euler = (math.pi, 0, angle) return {'FINISHED'} -class VIEW3D_OT_active_cam_to_vr_landmark(Operator): - bl_idname = "view3d.active_cam_to_vr_landmark" - bl_label = "Active Camera to Landmark" - bl_description = "Position the active camera at the selected landmark" +class VIEW3D_OT_camera_to_vr_landmark(Operator): + bl_idname = "view3d.camera_to_vr_landmark" + bl_label = "Scene Camera to VR Landmark" + bl_description = "Position the scene camera at the selected landmark" bl_options = {'UNDO', 'REGISTER'} @classmethod @@ -535,14 +535,15 @@ class VIEW3D_OT_active_cam_to_vr_landmark(Operator): return context.scene.camera is not None def execute(self, context): - scene = context.scene + import math - lm = scene.vr_landmarks[scene.vr_landmarks_selected] + scene = context.scene + lm = VRLandmark.get_selected_landmark(context) cam = scene.camera angle = lm.base_pose_angle cam.location = lm.base_pose_location - cam.rotation_euler = (1.5708, 0, angle) + cam.rotation_euler = (math.pi / 2, 0, angle) return {'FINISHED'} @@ -681,9 +682,9 @@ class VIEW3D_GGT_vr_viewer_pose(GizmoGroup): self.gizmo.matrix_basis = self._get_viewer_pose_matrix(context) -class VIEW3D_GGT_vr_viewer_viz(GizmoGroup): - bl_idname = "VIEW3D_GGT_vr_viewer_viz" - bl_label = "VR Landmark Indicator" +class VIEW3D_GGT_vr_landmarks(GizmoGroup): + bl_idname = "VIEW3D_GGT_vr_landmarks" + bl_label = "VR Landmark Indicators" bl_space_type = 'VIEW_3D' bl_region_type = 'WINDOW' bl_options = {'3D', 'PERSISTENT', 'SCALE'} @@ -695,24 +696,24 @@ class VIEW3D_GGT_vr_viewer_viz(GizmoGroup): view3d.shading.vr_show_landmarks ) - def setup(self,context): + def setup(self, context): pass def draw_prepare(self, context): - # first delete the old gizmos + # first delete the old gizmos for g in self.gizmos: self.gizmos.remove(g) from math import radians from mathutils import Matrix, Euler - landmarks = context.scene.vr_landmarks - - default_matrix = Matrix(((1.0, 0.0, 0.0, 0.0), - (0.0, 1.0, 0.0, 0.0), - (0.0, 0.0, 1.0, 0.0), - (0.0, 0.0, 0.0, 1.0))) + scene = context.scene + landmarks = scene.vr_landmarks for lm in landmarks: + if ((lm.type == 'SCENE_CAMERA' and not scene.camera) or + (lm.type == 'USER_CAMERA' and not lm.base_pose_camera)): + continue + gizmo = self.gizmos.new(VIEW3D_GT_vr_camera_cone.bl_idname) gizmo.aspect = 1 / 3, 1 / 4 @@ -722,10 +723,8 @@ class VIEW3D_GGT_vr_viewer_viz(GizmoGroup): self.gizmo = gizmo if lm.type == 'SCENE_CAMERA': - if context.scene.camera: - lm_mat = context.scene.camera.matrix_world - else: - lm_mat = default_matrix + cam = scene.camera + lm_mat = cam.matrix_world if cam else Matrix.Identity(4) elif lm.type == 'USER_CAMERA': lm_mat = lm.base_pose_camera.matrix_world else: @@ -757,15 +756,15 @@ classes = ( VIEW3D_OT_vr_landmark_remove, VIEW3D_OT_vr_landmark_activate, VIEW3D_OT_vr_landmark_from_session, - VIEW3d_OT_new_cam_to_vr_landmark, - VIEW3D_OT_active_cam_to_vr_landmark, + VIEW3d_OT_add_camera_from_vr_landmark, + VIEW3D_OT_camera_to_vr_landmark, VIEW3D_OT_vr_landmark_from_camera, VIEW3D_OT_cursor_to_vr_landmark, VIEW3D_OT_update_vr_landmark, VIEW3D_GT_vr_camera_cone, VIEW3D_GGT_vr_viewer_pose, - VIEW3D_GGT_vr_viewer_viz, + VIEW3D_GGT_vr_landmarks, ) @@ -793,7 +792,7 @@ def register(): name="Show VR Camera" ) bpy.types.View3DShading.vr_show_landmarks = BoolProperty( - name="Show Landmarks" + name="Show Landmarks" ) bpy.app.handlers.load_post.append(ensure_default_vr_landmark) -- cgit v1.2.3 From 25b00a0a52c81408b9dc15ea320a79ee956b3c0a Mon Sep 17 00:00:00 2001 From: Julian Eisel Date: Wed, 22 Jul 2020 14:10:56 +0200 Subject: VR Scene Inspection: Bump version number to 0.9 For the final 2.90 release this should go up to 1.0, but not doing that yet as we may have to do some fixes still. --- viewport_vr_preview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/viewport_vr_preview.py b/viewport_vr_preview.py index 7db9cea6..c7d1d1af 100644 --- a/viewport_vr_preview.py +++ b/viewport_vr_preview.py @@ -38,8 +38,8 @@ from bpy.app.handlers import persistent bl_info = { "name": "VR Scene Inspection", "author": "Julian Eisel (Severin), Sebastian Koenig", - "version": (0, 2, 0), - "blender": (2, 83, 0), + "version": (0, 9, 0), + "blender": (2, 90, 0), "location": "3D View > Sidebar > VR", "description": ("View the viewport with virtual reality glasses " "(head-mounted displays)"), -- cgit v1.2.3 From 799ee4aebb80f9620719c7f4204265492dec68bb Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Thu, 23 Jul 2020 07:31:31 +0200 Subject: glTF: Bump master to 1.4.0 after blender-v2.90-release branch creation --- io_scene_gltf2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 85fdde3c..bf96ff7d 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 42), + "version": (1, 4, 0), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', -- cgit v1.2.3 From 089cfd12a5511829aafad3790417a5218955a3ad Mon Sep 17 00:00:00 2001 From: Mikhail Rachinskiy Date: Thu, 23 Jul 2020 14:39:19 +0400 Subject: PLY: show import/export status with cursor Later I would like to show progress by percentage, but not yet sure on how to better calculate it and triger update. --- io_mesh_ply/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/io_mesh_ply/__init__.py b/io_mesh_ply/__init__.py index 2cfb09c3..1a69c346 100644 --- a/io_mesh_ply/__init__.py +++ b/io_mesh_ply/__init__.py @@ -84,6 +84,8 @@ class ImportPLY(bpy.types.Operator, ImportHelper): import os from . import import_ply + context.window.cursor_set('WAIT') + paths = [ os.path.join(self.directory, name.name) for name in self.files @@ -95,6 +97,8 @@ class ImportPLY(bpy.types.Operator, ImportHelper): for path in paths: import_ply.load(self, context, path) + context.window.cursor_set('DEFAULT') + return {'FINISHED'} @@ -150,6 +154,8 @@ class ExportPLY(bpy.types.Operator, ExportHelper): from mathutils import Matrix from . import export_ply + context.window.cursor_set('WAIT') + keywords = self.as_keywords( ignore=( "axis_forward", @@ -165,11 +171,10 @@ class ExportPLY(bpy.types.Operator, ExportHelper): ).to_4x4() @ Matrix.Scale(self.global_scale, 4) keywords["global_matrix"] = global_matrix - filepath = self.filepath - filepath = bpy.path.ensure_ext(filepath, self.filename_ext) - export_ply.save(context, **keywords) + context.window.cursor_set('DEFAULT') + return {'FINISHED'} def draw(self, context): -- cgit v1.2.3 From 2c9bc1e642bc77470921968b4890fc53a038f408 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Thu, 23 Jul 2020 23:15:35 -0400 Subject: Collection Manager: QCD Move Widget fixes. Task: T69577 Fix QCD Move Widget not accounting for the 3D View bounds when first called and not appearing at all when called from the menu if the mouse is outside the 3D View. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/qcd_move_widget.py | 43 +++++++++++++++++----------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 7fe71187..ad67c29b 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 1), + "version": (2, 12, 2), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/qcd_move_widget.py b/object_collection_manager/qcd_move_widget.py index 85f63f58..28ed9c01 100644 --- a/object_collection_manager/qcd_move_widget.py +++ b/object_collection_manager/qcd_move_widget.py @@ -27,10 +27,12 @@ import gpu from gpu_extras.batch import batch_for_shader from bpy.types import Operator + from .internals import ( layer_collections, qcd_slots, ) + from . import qcd_operators def spacer(): @@ -338,13 +340,7 @@ def mouse_in_area(mouse_pos, area, buf = 0): return True def account_for_view_bounds(area): - # make sure it renders in the 3d view - # left - if area["vert"][0] < 0: - x = 0 - y = area["vert"][1] - - area["vert"] = (x, y) + # make sure it renders in the 3d view - prioritize top left # right if area["vert"][0] + area["width"] > bpy.context.region.width: @@ -353,10 +349,10 @@ def account_for_view_bounds(area): area["vert"] = (x, y) - # top - if area["vert"][1] > bpy.context.region.height: - x = area["vert"][0] - y = bpy.context.region.height + # left + if area["vert"][0] < 0: + x = 0 + y = area["vert"][1] area["vert"] = (x, y) @@ -367,6 +363,13 @@ def account_for_view_bounds(area): area["vert"] = (x, y) + # top + if area["vert"][1] > bpy.context.region.height: + x = area["vert"][0] + y = bpy.context.region.height + + area["vert"] = (x, y) + def update_area_dimensions(area, w=0, h=0): area["width"] += w area["height"] += h @@ -390,6 +393,7 @@ class QCDMoveWidget(Operator): } last_type = '' + initialized = False moved = False def modal(self, context, event): @@ -424,12 +428,16 @@ class QCDMoveWidget(Operator): self.mouse_pos = (event.mouse_region_x, event.mouse_region_y) if not mouse_in_area(self.mouse_pos, self.areas["Main Window"], 50 * scale_factor()): - bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + if self.initialized: + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') - if self.moved: - bpy.ops.ed.undo_push() + if self.moved: + bpy.ops.ed.undo_push() - return {'FINISHED'} + return {'FINISHED'} + + else: + self.initialized = True elif event.value == 'PRESS' and event.type == 'LEFTMOUSE': if not mouse_in_area(self.mouse_pos, self.areas["Main Window"], 10 * scale_factor()): @@ -498,13 +506,14 @@ class QCDMoveWidget(Operator): "height": 0, "value": None } - account_for_view_bounds(main_window) - # add main window background to areas self.areas["Main Window"] = main_window + allocate_main_ui(self, context) + account_for_view_bounds(main_window) context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} + else: self.report({'WARNING'}, "View3D not found, cannot run operator") return {'CANCELLED'} -- cgit v1.2.3 From d1a009898e47c9b5761d99c511fa08e8aec71988 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Thu, 23 Jul 2020 23:15:35 -0400 Subject: Collection Manager: QCD Move Widget fixes. Task: T69577 Fix QCD Move Widget not accounting for the 3D View bounds when first called and not appearing at all when called from the menu if the mouse is outside the 3D View. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/qcd_move_widget.py | 43 +++++++++++++++++----------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 7fe71187..ad67c29b 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 1), + "version": (2, 12, 2), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/qcd_move_widget.py b/object_collection_manager/qcd_move_widget.py index 85f63f58..28ed9c01 100644 --- a/object_collection_manager/qcd_move_widget.py +++ b/object_collection_manager/qcd_move_widget.py @@ -27,10 +27,12 @@ import gpu from gpu_extras.batch import batch_for_shader from bpy.types import Operator + from .internals import ( layer_collections, qcd_slots, ) + from . import qcd_operators def spacer(): @@ -338,13 +340,7 @@ def mouse_in_area(mouse_pos, area, buf = 0): return True def account_for_view_bounds(area): - # make sure it renders in the 3d view - # left - if area["vert"][0] < 0: - x = 0 - y = area["vert"][1] - - area["vert"] = (x, y) + # make sure it renders in the 3d view - prioritize top left # right if area["vert"][0] + area["width"] > bpy.context.region.width: @@ -353,10 +349,10 @@ def account_for_view_bounds(area): area["vert"] = (x, y) - # top - if area["vert"][1] > bpy.context.region.height: - x = area["vert"][0] - y = bpy.context.region.height + # left + if area["vert"][0] < 0: + x = 0 + y = area["vert"][1] area["vert"] = (x, y) @@ -367,6 +363,13 @@ def account_for_view_bounds(area): area["vert"] = (x, y) + # top + if area["vert"][1] > bpy.context.region.height: + x = area["vert"][0] + y = bpy.context.region.height + + area["vert"] = (x, y) + def update_area_dimensions(area, w=0, h=0): area["width"] += w area["height"] += h @@ -390,6 +393,7 @@ class QCDMoveWidget(Operator): } last_type = '' + initialized = False moved = False def modal(self, context, event): @@ -424,12 +428,16 @@ class QCDMoveWidget(Operator): self.mouse_pos = (event.mouse_region_x, event.mouse_region_y) if not mouse_in_area(self.mouse_pos, self.areas["Main Window"], 50 * scale_factor()): - bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + if self.initialized: + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') - if self.moved: - bpy.ops.ed.undo_push() + if self.moved: + bpy.ops.ed.undo_push() - return {'FINISHED'} + return {'FINISHED'} + + else: + self.initialized = True elif event.value == 'PRESS' and event.type == 'LEFTMOUSE': if not mouse_in_area(self.mouse_pos, self.areas["Main Window"], 10 * scale_factor()): @@ -498,13 +506,14 @@ class QCDMoveWidget(Operator): "height": 0, "value": None } - account_for_view_bounds(main_window) - # add main window background to areas self.areas["Main Window"] = main_window + allocate_main_ui(self, context) + account_for_view_bounds(main_window) context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} + else: self.report({'WARNING'}, "View3D not found, cannot run operator") return {'CANCELLED'} -- cgit v1.2.3 From 03e3ef7f71f20a370ef4302455f36bc476f3f33a Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Fri, 24 Jul 2020 08:09:38 +0200 Subject: glTF importer: fix regression for skinned mesh normals not being renormalized --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/imp/gltf2_blender_mesh.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 85fdde3c..70a06504 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 42), + "version": (1, 3, 43), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py index 175bc920..e393eb86 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -540,6 +540,7 @@ def skin_into_bind_pose(gltf, skin_idx, vert_joints, vert_weights, locs, vert_no if len(vert_normals) != 0: vert_normals[:] = mul_mats_vecs(skinning_mats_3x3, vert_normals) # Don't translate normals! + normalize_vecs(vert_normals) def mul_mats_vecs(mats, vecs): @@ -547,6 +548,11 @@ def mul_mats_vecs(mats, vecs): return np.matmul(mats, vecs.reshape(len(vecs), 3, 1)).reshape(len(vecs), 3) +def normalize_vecs(vectors): + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + np.divide(vectors, norms, out=vectors, where=norms != 0) + + def merge_duplicate_verts(vert_locs, vert_normals, vert_joints, vert_weights, sk_vert_locs, loop_vidxs, edge_vidxs): # This function attempts to invert the splitting done when exporting to # glTF. Welds together verts with the same per-vert data (but possibly -- cgit v1.2.3 From c0d9f77449abcaf947c76a0d19081c076aae3bc9 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Fri, 24 Jul 2020 08:13:13 +0200 Subject: Merge branch 'blender-v2.90-release' --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/imp/gltf2_blender_mesh.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index bf96ff7d..7199ea6a 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 4, 0), + "version": (1, 4, 1), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py index 175bc920..e393eb86 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -540,6 +540,7 @@ def skin_into_bind_pose(gltf, skin_idx, vert_joints, vert_weights, locs, vert_no if len(vert_normals) != 0: vert_normals[:] = mul_mats_vecs(skinning_mats_3x3, vert_normals) # Don't translate normals! + normalize_vecs(vert_normals) def mul_mats_vecs(mats, vecs): @@ -547,6 +548,11 @@ def mul_mats_vecs(mats, vecs): return np.matmul(mats, vecs.reshape(len(vecs), 3, 1)).reshape(len(vecs), 3) +def normalize_vecs(vectors): + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + np.divide(vectors, norms, out=vectors, where=norms != 0) + + def merge_duplicate_verts(vert_locs, vert_normals, vert_joints, vert_weights, sk_vert_locs, loop_vidxs, edge_vidxs): # This function attempts to invert the splitting done when exporting to # glTF. Welds together verts with the same per-vert data (but possibly -- cgit v1.2.3 From c52cfd99ff31f7554cc998c69382d1c8dd7ed8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 15 Jul 2020 00:39:45 +0200 Subject: BlenderKit: fix login after token refresh fails. Now offers a popup to login on site, previously only reported about invalid token, which wasn't clear to many users. --- blenderkit/bkit_oauth.py | 4 ++-- blenderkit/rerequests.py | 5 +++++ blenderkit/search.py | 4 ++-- blenderkit/tasks_queue.py | 13 +++++++++---- blenderkit/ui.py | 34 +++------------------------------- blenderkit/utils.py | 33 +++++++++++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 39 deletions(-) diff --git a/blenderkit/bkit_oauth.py b/blenderkit/bkit_oauth.py index ae90b215..59ed6c8b 100644 --- a/blenderkit/bkit_oauth.py +++ b/blenderkit/bkit_oauth.py @@ -116,7 +116,7 @@ class RegisterLoginOnline(bpy.types.Operator): message: bpy.props.StringProperty( name="Message", description="", - default="You were logged out from BlenderKit. Clicking OK takes you to web login. ") + default="You were logged out from BlenderKit.\n Clicking OK takes you to web login. ") @classmethod def poll(cls, context): @@ -124,7 +124,7 @@ class RegisterLoginOnline(bpy.types.Operator): def draw(self, context): layout = self.layout - utils.label_multiline(layout, text=self.message) + utils.label_multiline(layout, text=self.message, width = 300) def execute(self, context): preferences = bpy.context.preferences.addons['blenderkit'].preferences diff --git a/blenderkit/rerequests.py b/blenderkit/rerequests.py index 3d9a4d75..c655c8c5 100644 --- a/blenderkit/rerequests.py +++ b/blenderkit/rerequests.py @@ -76,6 +76,11 @@ def rerequest(method, url, **kwargs): utils.p('reresult', response.status_code) if response.status_code >= 400: utils.p('reresult', response.text) + else: + tasks_queue.add_task((ui.add_report, ( + 'Refreshing token failed.Please login manually.', 10))) + # tasks_queue.add_task((bkit_oauth.write_tokens, ('', '', ''))) + tasks_queue.add_task((bpy.ops.wm.blenderkit_login,( 'INVOKE_DEFAULT',)),fake_context = True) return response diff --git a/blenderkit/search.py b/blenderkit/search.py index 09dfeb65..f6226049 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -72,7 +72,7 @@ def check_errors(rdata): if user_preferences.enable_oauth: bkit_oauth.refresh_token_thread() return False, rdata.get('detail') - return False, 'Missing or wrong api_key in addon preferences' + return False, 'Use login panel to connect your profile.' return True, '' @@ -282,7 +282,7 @@ def timer_update(): search() preferences.first_run = False if preferences.tips_on_start: - ui.get_largest_3dview() + utils.get_largest_3dview() ui.update_ui_size(ui.active_area, ui.active_region) ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN) return 3.0 diff --git a/blenderkit/tasks_queue.py b/blenderkit/tasks_queue.py index bbac6d63..a253aa96 100644 --- a/blenderkit/tasks_queue.py +++ b/blenderkit/tasks_queue.py @@ -45,15 +45,16 @@ def get_queue(): return t.task_queue class task_object: - def __init__(self, command = '', arguments = (), wait = 0, only_last = False): + def __init__(self, command = '', arguments = (), wait = 0, only_last = False, fake_context = False): self.command = command self.arguments = arguments self.wait = wait self.only_last = only_last + self.fake_context = fake_context -def add_task(task, wait = 0, only_last = False): +def add_task(task, wait = 0, only_last = False, fake_context = False): q = get_queue() - taskob = task_object(task[0],task[1], wait = wait, only_last = only_last) + taskob = task_object(task[0],task[1], wait = wait, only_last = only_last, fake_context = fake_context) q.put(taskob) @@ -90,7 +91,11 @@ def queue_worker(): utils.p('as a task: ') utils.p(task.command, task.arguments) try: - task.command(*task.arguments) + if task.fake_context: + fc = utils.get_fake_context(bpy.context) + task.command(fc,*task.arguments) + else: + task.command(*task.arguments) except Exception as e: utils.p('task failed:') print(e) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index a1cd66d9..fa26d8a3 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1181,30 +1181,6 @@ def update_ui_size(area, region): ui.rating_y = ui.bar_y - ui.bar_height -def get_largest_3dview(): - maxsurf = 0 - maxa = None - maxw = None - region = None - for w in bpy.context.window_manager.windows: - screen = w.screen - for a in screen.areas: - if a.type == 'VIEW_3D': - asurf = a.width * a.height - if asurf > maxsurf: - maxa = a - maxw = w - maxsurf = asurf - - for r in a.regions: - if r.type == 'WINDOW': - region = r - global active_area, active_window, active_region - active_window = maxw - active_area = maxa - active_region = region - return maxw, maxa, region - class AssetBarOperator(bpy.types.Operator): '''runs search and displays the asset bar at the same time''' @@ -1808,13 +1784,14 @@ class UndoWithContext(bpy.types.Operator): C_dict = bpy.context.copy() C_dict.update(region='WINDOW') if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = get_largest_3dview() + w, a, r = utils.get_largest_3dview() override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) bpy.ops.ed.undo_push(C_dict, 'INVOKE_REGION_WIN', message=self.message) return {'FINISHED'} + class RunAssetBarWithContext(bpy.types.Operator): """Regenerate cobweb""" bl_idname = "object.run_assetbar_fix_context" @@ -1826,12 +1803,7 @@ class RunAssetBarWithContext(bpy.types.Operator): # return {'RUNNING_MODAL'} def execute(self, context): - C_dict = bpy.context.copy() - C_dict.update(region='WINDOW') - if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = get_largest_3dview() - override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} - C_dict.update(override) + C_dict = utils.get_fake_context(context) bpy.ops.view3d.blenderkit_asset_bar(C_dict, 'INVOKE_REGION_WIN', keep_running=True, do_search=False) return {'FINISHED'} diff --git a/blenderkit/utils.py b/blenderkit/utils.py index 2e59887c..78eff216 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -614,6 +614,39 @@ def guard_from_crash(): return True +def get_largest_3dview(): + maxsurf = 0 + maxa = None + maxw = None + region = None + for w in bpy.context.window_manager.windows: + screen = w.screen + for a in screen.areas: + if a.type == 'VIEW_3D': + asurf = a.width * a.height + if asurf > maxsurf: + maxa = a + maxw = w + maxsurf = asurf + + for r in a.regions: + if r.type == 'WINDOW': + region = r + global active_area, active_window, active_region + active_window = maxw + active_area = maxa + active_region = region + return maxw, maxa, region + +def get_fake_context(context): + C_dict = context.copy() + C_dict.update(region='WINDOW') + if context.area is None or context.area.type != 'VIEW_3D': + w, a, r = get_largest_3dview() + override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} + C_dict.update(override) + return C_dict + def label_multiline(layout, text='', icon='NONE', width=-1): ''' draw a ui label, but try to split it in multiple lines.''' if text.strip() == '': -- cgit v1.2.3 From 00fefe2d147288e3a218d640668b54331e82d3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 15 Jul 2020 19:06:53 +0200 Subject: BlenderKit: on-registration popup This popup informs the user that BlenderKit connects to the internet directly after registration, and asks for consent with it and also performs first search. --- blenderkit/__init__.py | 5 +++++ blenderkit/search.py | 8 +++---- blenderkit/tasks_queue.py | 9 ++++---- blenderkit/ui.py | 2 +- blenderkit/ui_panels.py | 56 +++++++++++++++++++++++++++++++++++++++++++++-- blenderkit/utils.py | 16 ++++++++------ 6 files changed, 78 insertions(+), 18 deletions(-) diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index a7e148ea..23ee89c5 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -1725,6 +1725,11 @@ def register(): bpy.app.timers.register(check_timers_timer, persistent=True) bpy.app.handlers.load_post.append(scene_load) + # detect if the user just enabled the addon in preferences, thus enable to run + for w in bpy.context.window_manager.windows: + for a in w.screen.areas: + if a.type == 'PREFERENCES': + tasks_queue.add_task((bpy.ops.wm.blenderkit_welcome,( 'INVOKE_DEFAULT',)),fake_context = True, fake_context_area = 'PREFERENCES') def unregister(): diff --git a/blenderkit/search.py b/blenderkit/search.py index f6226049..cee0b14b 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -282,14 +282,14 @@ def timer_update(): search() preferences.first_run = False if preferences.tips_on_start: - utils.get_largest_3dview() + utils.get_largest_area() ui.update_ui_size(ui.active_area, ui.active_region) ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN) return 3.0 - if preferences.first_run: - search() - preferences.first_run = False + # if preferences.first_run: + # search() + # preferences.first_run = False # check_clipboard() diff --git a/blenderkit/tasks_queue.py b/blenderkit/tasks_queue.py index a253aa96..5a327290 100644 --- a/blenderkit/tasks_queue.py +++ b/blenderkit/tasks_queue.py @@ -45,16 +45,17 @@ def get_queue(): return t.task_queue class task_object: - def __init__(self, command = '', arguments = (), wait = 0, only_last = False, fake_context = False): + def __init__(self, command = '', arguments = (), wait = 0, only_last = False, fake_context = False, fake_context_area = 'VIEW_3D'): self.command = command self.arguments = arguments self.wait = wait self.only_last = only_last self.fake_context = fake_context + self.fake_context_area = fake_context_area -def add_task(task, wait = 0, only_last = False, fake_context = False): +def add_task(task, wait = 0, only_last = False, fake_context = False, fake_context_area = 'VIEW_3D'): q = get_queue() - taskob = task_object(task[0],task[1], wait = wait, only_last = only_last, fake_context = fake_context) + taskob = task_object(task[0],task[1], wait = wait, only_last = only_last, fake_context = fake_context, fake_context_area = fake_context_area) q.put(taskob) @@ -92,7 +93,7 @@ def queue_worker(): utils.p(task.command, task.arguments) try: if task.fake_context: - fc = utils.get_fake_context(bpy.context) + fc = utils.get_fake_context(bpy.context, area_type = task.fake_context_area) task.command(fc,*task.arguments) else: task.command(*task.arguments) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index fa26d8a3..30195168 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1784,7 +1784,7 @@ class UndoWithContext(bpy.types.Operator): C_dict = bpy.context.copy() C_dict.update(region='WINDOW') if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = utils.get_largest_3dview() + w, a, r = utils.get_largest_area() override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) bpy.ops.ed.undo_push(C_dict, 'INVOKE_REGION_WIN', message=self.message) diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index b8d8ce73..d067afa0 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -25,12 +25,22 @@ if "bpy" in locals(): download = importlib.reload(download) categories = importlib.reload(categories) icons = importlib.reload(icons) + icons = importlib.reload(search) else: - from blenderkit import paths, ratings, utils, download, categories, icons + from blenderkit import paths, ratings, utils, download, categories, icons, search from bpy.types import ( Panel ) +from bpy.props import ( + IntProperty, + FloatProperty, + FloatVectorProperty, + StringProperty, + EnumProperty, + BoolProperty, + PointerProperty, +) import bpy import os @@ -962,6 +972,47 @@ class VIEW3D_PT_blenderkit_unified(Panel): if ui_props.asset_type == 'TEXTURE': layout.label(text='not yet implemented') +class BlenderKitWelcomeOperator(bpy.types.Operator): + """Login online on BlenderKit webpage""" + + bl_idname = "wm.blenderkit_welcome" + bl_label = "Welcome to BlenderKit!" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + step: IntProperty( + name="step", + description="Tutorial Step", + default=0, + options={'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + if self.step == 0: + message = "BlenderKit is an addon that connects to the internet to search and upload for models, materials, and brushes. \n\n Let's start by searching for some cool materials?" + else: + message = "This shouldn't be here at all" + utils.label_multiline(layout, text= message, width = 300) + + def execute(self, context): + if self.step == 0: + #move mouse: + #bpy.context.window_manager.windows[0].cursor_warp(1000, 1000) + #show n-key sidebar (spaces[index] has to be found for view3d too: + # bpy.context.window_manager.windows[0].screen.areas[5].spaces[0].show_region_ui = False + print('running search no') + ui_props = bpy.context.scene.blenderkitUI + ui_props.asset_type = 'MATERIAL' + search.search() + return {'FINISHED'} + + def invoke(self, context, event): + wm = bpy.context.window_manager + return wm.invoke_props_dialog(self) def draw_asset_context_menu(self, context, asset_data): layout = self.layout @@ -1298,7 +1349,8 @@ classess = ( VIEW3D_PT_blenderkit_downloads, OBJECT_MT_blenderkit_asset_menu, OBJECT_MT_blenderkit_login_menu, - UrlPopupDialog + UrlPopupDialog, + BlenderKitWelcomeOperator, ) diff --git a/blenderkit/utils.py b/blenderkit/utils.py index 78eff216..effc2627 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -614,15 +614,14 @@ def guard_from_crash(): return True -def get_largest_3dview(): +def get_largest_area( area_type = 'VIEW_3D'): maxsurf = 0 maxa = None maxw = None region = None for w in bpy.context.window_manager.windows: - screen = w.screen - for a in screen.areas: - if a.type == 'VIEW_3D': + for a in w.screen.areas: + if a.type == area_type: asurf = a.width * a.height if asurf > maxsurf: maxa = a @@ -638,15 +637,18 @@ def get_largest_3dview(): active_region = region return maxw, maxa, region -def get_fake_context(context): +def get_fake_context(context, area_type = 'VIEW_3D'): C_dict = context.copy() C_dict.update(region='WINDOW') - if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = get_largest_3dview() + if context.area is None or context.area.type != area_type: + w, a, r = get_largest_area(area_type = area_type) + override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) + # print(w,a,r) return C_dict + def label_multiline(layout, text='', icon='NONE', width=-1): ''' draw a ui label, but try to split it in multiple lines.''' if text.strip() == '': -- cgit v1.2.3 From 8f6903bc92531aa8e5d4c64a0a108c2904506a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Mon, 27 Jul 2020 12:59:38 +0200 Subject: BlenderKit: Rating refactorings This mainly paves a way for removing the old and clumsy bgl UI and enable faster rating for users. -Rating Ui is now more responsive -it can be dragged over the stars widget. -fast rating operator (f shortcut over assetbar) -wip on new ratings panel(disabled by now) -if author didn't provide his webpage, the link now leads to his profile on the BlenderKit site. -upload was partially broken thanks to a small bug -perpendicular snap - This limits angled snapping in a reasonable way, should help when placing foliage or items on sloped ceilings e.t.c. -removed the first_run property, it's replaced with a poput that informs the user about connecting to the internet. --- blenderkit/__init__.py | 64 +++++------- blenderkit/paths.py | 3 + blenderkit/ratings.py | 269 ++++++++++++++++++++++++++++++++++++++---------- blenderkit/search.py | 10 +- blenderkit/ui.py | 28 +++-- blenderkit/ui_panels.py | 95 ++++++++++------- blenderkit/upload.py | 2 +- 7 files changed, 328 insertions(+), 143 deletions(-) diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index 23ee89c5..fba80a7e 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -250,6 +250,7 @@ def switch_search_results(self, context): s['search results orig'] = s.get('bkit brush search orig') search.load_previews() + def asset_type_callback(self, context): ''' Returns @@ -650,29 +651,6 @@ class BlenderKitCommonUploadProps(object): ) -def stars_enum_callback(self, context): - items = [] - for a in range(0, 10): - if self.rating_quality < a+1: - icon = 'SOLO_OFF' - else: - icon = 'SOLO_ON' - # has to have something before the number in the value, otherwise fails on registration. - items.append((f'{a+1}', f'{a+1}', '', icon, a+1)) - return items - - -def update_quality(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key == '': - # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') - # return - bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', message = 'Please login/signup to rate assets. Clicking OK takes you to web login.') - self.rating_quality_ui = '0' - self.rating_quality = int(self.rating_quality_ui) - - class BlenderKitRatingProps(PropertyGroup): rating_quality: IntProperty(name="Quality", description="quality of the material", @@ -680,19 +658,20 @@ class BlenderKitRatingProps(PropertyGroup): min=-1, max=10, update=ratings.update_ratings_quality) - #the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. + # the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. rating_quality_ui: EnumProperty(name='rating_quality_ui', - items=stars_enum_callback, - description='Rating stars 0 - 10', - default=None, - update=update_quality, - ) + items=ratings.stars_enum_callback, + description='Rating stars 0 - 10', + default=None, + update=ratings.update_quality_ui, + ) rating_work_hours: FloatProperty(name="Work Hours", description="How many hours did this work take?", default=0.00, min=0.0, max=1000, update=ratings.update_ratings_work_hours ) + # rating_complexity: IntProperty(name="Complexity", # description="Complexity is a number estimating how much work was spent on the asset.aaa", # default=0, min=0, max=10) @@ -1393,6 +1372,17 @@ class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps): max=180, subtype='ANGLE') + perpendicular_snap: BoolProperty(name='Perpendicular snap', + description="Limit snapping that is close to perpendicular angles to be perpendicular.", + default=True) + + perpendicular_snap_threshold: FloatProperty(name="Threshold", + description="Limit perpendicular snap to be below these values.", + default=.25, + min=0, + max=.5, + ) + class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps): search_keywords: StringProperty( @@ -1586,12 +1576,13 @@ class BlenderKitAddonPreferences(AddonPreferences): min=0, max=20000) - first_run: BoolProperty( - name="First run", - description="Detects if addon was already registered/run.", - default=True, - update=utils.save_prefs - ) + # this is now made obsolete by the new popup upon registration -ensures the user knows about the first search. + # first_run: BoolProperty( + # name="First run", + # description="Detects if addon was already registered/run.", + # default=True, + # update=utils.save_prefs + # ) use_timers: BoolProperty( name="Use timers", @@ -1729,7 +1720,8 @@ def register(): for w in bpy.context.window_manager.windows: for a in w.screen.areas: if a.type == 'PREFERENCES': - tasks_queue.add_task((bpy.ops.wm.blenderkit_welcome,( 'INVOKE_DEFAULT',)),fake_context = True, fake_context_area = 'PREFERENCES') + tasks_queue.add_task((bpy.ops.wm.blenderkit_welcome, ('INVOKE_DEFAULT',)), fake_context=True, + fake_context_area='PREFERENCES') def unregister(): diff --git a/blenderkit/paths.py b/blenderkit/paths.py index b4210a85..399e7555 100644 --- a/blenderkit/paths.py +++ b/blenderkit/paths.py @@ -75,6 +75,9 @@ def get_api_url(): def get_oauth_landing_url(): return get_bkit_url() + BLENDERKIT_OAUTH_LANDING_URL +def get_author_gallery_url(author_id): + return f'{get_bkit_url()}/asset-gallery?query=author_id:{author_id}' + def default_global_dict(): from os.path import expanduser diff --git a/blenderkit/ratings.py b/blenderkit/ratings.py index 48c34a61..800749c8 100644 --- a/blenderkit/ratings.py +++ b/blenderkit/ratings.py @@ -94,7 +94,7 @@ def upload_review_thread(url, reviews, headers): def get_rating(asset_id): - #this function isn't used anywhere,should probably get removed. + # this function isn't used anywhere,should probably get removed. user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key headers = utils.get_headers(api_key) @@ -114,8 +114,13 @@ def update_ratings_quality(self, context): headers = utils.get_headers(api_key) asset = self.id_data - bkit_ratings = asset.bkit_ratings - url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + if asset: + bkit_ratings = asset.bkit_ratings + url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + else: + # this part is for operator rating: + bkit_ratings = self + url = paths.get_api_url() + f'assets/{self.asset_id}/rating/' if bkit_ratings.rating_quality > 0.1: ratings = [('quality', bkit_ratings.rating_quality)] @@ -127,15 +132,19 @@ def update_ratings_work_hours(self, context): api_key = user_preferences.api_key headers = utils.get_headers(api_key) asset = self.id_data - bkit_ratings = asset.bkit_ratings - url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + if asset: + bkit_ratings = asset.bkit_ratings + url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + else: + # this part is for operator rating: + bkit_ratings = self + url = paths.get_api_url() + f'assets/{self.asset_id}/rating/' if bkit_ratings.rating_work_hours > 0.05: ratings = [('working_hours', round(bkit_ratings.rating_work_hours, 1))] tasks_queue.add_task((send_rating_to_thread_work_hours, (url, ratings, headers)), wait=1, only_last=True) - def upload_rating(asset): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key @@ -173,6 +182,7 @@ def upload_rating(asset): if bkit_ratings.rating_quality > 0.1 and bkit_ratings.rating_work_hours > 0.1: s['assets rated'][asset['asset_data']['assetBaseId']] = True + def get_assets_for_rating(): ''' gets assets from scene that could/should be rated by the user. @@ -191,26 +201,6 @@ def get_assets_for_rating(): assets.append(b) return assets -# class StarRatingOperator(bpy.types.Operator): -# """Tooltip""" -# bl_idname = "object.blenderkit_rating" -# bl_label = "Rate the Asset Quality" -# bl_options = {'REGISTER', 'INTERNAL'} -# -# property_name: StringProperty( -# name="Rating Property", -# description="Property that is rated", -# default="", -# ) -# -# rating: IntProperty(name="Rating", description="rating value", default=1, min=1, max=10) -# -# def execute(self, context): -# asset = utils.get_active_asset() -# props = asset.bkit_ratings -# props.rating_quality = self.rating -# return {'FINISHED'} - asset_types = ( ('MODEL', 'Model', 'set of objects'), @@ -254,43 +244,212 @@ class UploadRatingOperator(bpy.types.Operator): return wm.invoke_props_dialog(self) +def stars_enum_callback(self, context): + '''regenerates the enum property used to display rating stars, so that there are filled/empty stars correctly.''' + items = [] + for a in range(0, 10): + if self.rating_quality < a + 1: + icon = 'SOLO_OFF' + else: + icon = 'SOLO_ON' + # has to have something before the number in the value, otherwise fails on registration. + items.append((f'{a + 1}', f'{a + 1}', '', icon, a + 1)) + return items + -def draw_rating(layout, props, prop_name, name): - # layout.label(name) - - row = layout.row(align=True) - # test method - 10 booleans. - # propsx = bpy.context.active_object.bkit_ratings - # for a in range(0, 10): - # pn = f'rq{str(a+1).zfill(2)}' - # if eval('propsx.' + pn) == False: - # icon = 'SOLO_OFF' - # else: - # icon = 'SOLO_ON' - # row.prop(propsx, pn, icon=icon, icon_only=True) - # print(dir(props)) - # new best method - enum with an items callback. ('animates' the stars as item icons) - row.prop(props, 'rating_quality_ui', expand=True, icon_only=True, emboss = False) - # original (operator) method: - # row = layout.row(align=True) - # for a in range(0, 10): - # if eval('props.' + prop_name) < a + 1: - # icon = 'SOLO_OFF' - # else: - # icon = 'SOLO_ON' - # - # op = row.operator('object.blenderkit_rating', icon=icon, emboss=False, text='') - # op.property_name = prop_name - # op.rating = a + 1 +def update_quality_ui(self, context): + '''Converts the _ui the enum into actual quality number.''' + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + if user_preferences.api_key == '': + # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') + # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') + # return + bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', + message='Please login/signup to rate assets. Clicking OK takes you to web login.') + self.rating_quality_ui = '0' + self.rating_quality = int(self.rating_quality_ui) +def update_ratings_work_hours_ui(self, context): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + if user_preferences.api_key == '': + # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') + # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') + # return + bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', + message='Please login/signup to rate assets. Clicking OK takes you to web login.') + self.rating_work_hours_ui = '0' + self.rating_work_hours = float(self.rating_work_hours_ui) + +def update_ratings_work_hours_ui_1_5(self, context): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + if user_preferences.api_key == '': + # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') + # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') + # return + bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', + message='Please login/signup to rate assets. Clicking OK takes you to web login.') + self.update_ratings_work_hours_ui_1_5 = '0' + self.rating_work_hours = float(self.update_ratings_work_hours_ui_1_5) + + + +class FastRateMenu(Operator): + """Fast rating of the assets directly in the asset bar - without need to download assets.""" + bl_idname = "wm.blenderkit_menu_rating_upload" + bl_label = "Send Rating" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + message: StringProperty( + name="message", + description="message", + default="Rating asset") + + asset_id: StringProperty( + name="Asset Base Id", + description="Unique name of the asset (hidden)", + default="") + + asset_type: StringProperty( + name="Asset type", + description="asset type", + default="") + + rating_quality: IntProperty(name="Quality", + description="quality of the material", + default=0, + min=-1, max=10, + update=update_ratings_quality) + + # the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. + rating_quality_ui: EnumProperty(name='rating_quality_ui', + items=stars_enum_callback, + description='Rating stars 0 - 10', + default=None, + update=update_quality_ui, + ) + + rating_work_hours: FloatProperty(name="Work Hours", + description="How many hours did this work take?", + default=0.00, + min=0.0, max=1000, update=update_ratings_work_hours + ) + + rating_work_hours_ui: EnumProperty(name="Work Hours", + description="How many hours did this work take?", + items=[('0', '0', ''), + ('.5', '0.5', ''), + ('1', '1', ''), + ('2', '2', ''), + ('3', '3', ''), + ('4', '4', ''), + ('5', '5', ''), + ('10', '10', ''), + ('15', '15', ''), + ('20', '20', ''), + ('50', '50', ''), + ('100', '100', ''), + ], + default='0', update=update_ratings_work_hours_ui + ) + + rating_work_hours_ui_1_5: EnumProperty(name="Work Hours", + description="How many hours did this work take?", + items=[('0', '0', ''), + ('.2', '0.2', ''), + ('.5', '0.5', ''), + ('1', '1', ''), + ('2', '2', ''), + ('3', '3', ''), + ('4', '4', ''), + ('5', '5', '') + ], + default='0', update=update_ratings_work_hours_ui_1_5 + ) + + @classmethod + def poll(cls, context): + scene = bpy.context.scene + ui_props = scene.blenderkitUI + return ui_props.active_index > -1 + + def draw(self, context): + layout = self.layout + col = layout.column() + + # layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0) + col.label(text=self.message) + row = col.row() + row.prop(self, 'rating_quality_ui', expand=True, icon_only=True, emboss=False) + col.separator() + col.prop(self, 'rating_work_hours') + row = col.row() + if self.asset_type == 'model': + row.prop(self, 'rating_work_hours_ui', expand=True, icon_only=False, emboss=True) + else: + row.prop(self, 'rating_work_hours_ui_1_5', expand=True, icon_only=False, emboss=True) + + def execute(self, context): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + api_key = user_preferences.api_key + headers = utils.get_headers(api_key) + + url = paths.get_api_url() + f'assets/{self.asset_id}/rating/' + + rtgs = [ + + ] + + self.rating_quality = int(self.rating_quality_ui) + + if self.rating_quality > 0.1: + rtgs.append(('quality', self.rating_quality)) + if self.rating_work_hours > 0.1: + rtgs.append(('working_hours', round(self.rating_work_hours, 1))) + + thread = threading.Thread(target=upload_rating_thread, args=(url, rtgs, headers)) + thread.start() + return {'FINISHED'} + + def invoke(self, context, event): + scene = bpy.context.scene + ui_props = scene.blenderkitUI + if ui_props.active_index > -1: + sr = bpy.context.scene['search results'] + asset_data = dict(sr[ui_props.active_index]) + self.asset_id = asset_data['id'] + self.asset_type = asset_data['assetType'] + self.message = f"Rate asset {asset_data['name']}" + wm = context.window_manager + return wm.invoke_props_dialog(self) + + +def rating_menu_draw(self, context): + layout = self.layout + + ui_props = context.scene.blenderkitUI + sr = bpy.context.scene['search results orig'] + + asset_search_index = ui_props.active_index + if asset_search_index > -1: + asset_data = dict(sr['results'][asset_search_index]) + + col = layout.column() + layout.label(text='Admin rating Tools:') + col.operator_context = 'INVOKE_DEFAULT' + + op = col.operator('wm.blenderkit_menu_rating_upload', text='Fast rate') + op.asset_id = asset_data['id'] + op.asset_type = asset_data['assetType'] + def register_ratings(): - pass; - # bpy.utils.register_class(StarRatingOperator) bpy.utils.register_class(UploadRatingOperator) + bpy.utils.register_class(FastRateMenu) + # bpy.types.OBJECT_MT_blenderkit_asset_menu.append(rating_menu_draw) def unregister_ratings(): pass; # bpy.utils.unregister_class(StarRatingOperator) bpy.utils.unregister_class(UploadRatingOperator) + bpy.utils.unregister_class(FastRateMenu) diff --git a/blenderkit/search.py b/blenderkit/search.py index cee0b14b..6c8d16e2 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -280,7 +280,7 @@ def timer_update(): # TODO here it should check if there are some results, and only open assetbar if this is the case, not search. # if bpy.context.scene.get('search results') is None: search() - preferences.first_run = False + # preferences.first_run = False if preferences.tips_on_start: utils.get_largest_area() ui.update_ui_size(ui.active_area, ui.active_region) @@ -1161,14 +1161,6 @@ def search(category='', get_next=False, author_id=''): scene = bpy.context.scene ui_props = scene.blenderkitUI - ### updating of search categories was moved here, due to the reason that BlenderKit created the blenderkit_data - # folder upon registration of BlenderKit, which wasn't a favourite option for some users (devs running tests). - # user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - # if not user_preferences.first_run: - # api_key = user_preferences.api_key - # if bpy.context.window_manager.get('bkit_categories') is None: - # categories.fetch_categories_thread(api_key) - if ui_props.asset_type == 'MODEL': if not hasattr(scene, 'blenderkit'): return; diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 30195168..7935363d 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -876,7 +876,8 @@ def draw_callback_2d_search(self, context): else: iname = utils.previmg_name(ui_props.active_index) img = bpy.data.images.get(iname) - img.colorspace_settings.name = 'sRGB' + if img: + img.colorspace_settings.name = 'sRGB' gimg = None atip = '' @@ -931,9 +932,21 @@ def mouse_raycast(context, mx, my): # rote = mathutils.Euler((0, 0, math.pi)) randoffset = math.pi if has_hit: - snapped_rotation = snapped_normal.to_track_quat('Z', 'Y').to_euler() - up = Vector((0, 0, 1)) props = bpy.context.scene.blenderkit_models + up = Vector((0, 0, 1)) + + if props.perpendicular_snap: + if snapped_normal.z > 1 - props.perpendicular_snap_threshold: + snapped_normal = Vector((0, 0, 1)) + elif snapped_normal.z < -1 + props.perpendicular_snap_threshold: + snapped_normal = Vector((0, 0, -1)) + elif abs(snapped_normal.z) < props.perpendicular_snap_threshold: + snapped_normal.z = 0 + snapped_normal.normalize() + + snapped_rotation = snapped_normal.to_track_quat('Z', 'Y').to_euler() + + if props.randomize_rotation and snapped_normal.angle(up) < math.radians(10.0): randoffset = props.offset_rotation_amount + math.pi + ( random.random() - 0.5) * props.randomize_rotation_amount @@ -1668,6 +1681,7 @@ class AssetBarOperator(bpy.types.Operator): utils.p('author:', a) search.search(author_id=a) return {'RUNNING_MODAL'} + if event.type == 'X' and ui_props.active_index > -1: # delete downloaded files for this asset sr = bpy.context.scene['search results'] @@ -1845,13 +1859,15 @@ def register_ui(): if not wm.keyconfigs.addon: return km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type='EMPTY') + #asset bar shortcut kmi = km.keymap_items.new(AssetBarOperator.bl_idname, 'SEMI_COLON', 'PRESS', ctrl=False, shift=False) kmi.properties.keep_running = False kmi.properties.do_search = False addon_keymapitems.append(kmi) - # auto open after searching: - kmi = km.keymap_items.new(RunAssetBarWithContext.bl_idname, 'SEMI_COLON', 'PRESS', \ - ctrl=True, shift=True, alt=True) + #fast rating shortcut + wm = bpy.context.window_manager + km = wm.keyconfigs.addon.keymaps['Window'] + kmi = km.keymap_items.new(ratings.FastRateMenu.bl_idname, 'F', 'PRESS', ctrl=False, shift=False) addon_keymapitems.append(kmi) diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index d067afa0..89646862 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -47,7 +47,6 @@ import os import random - # this was moved to separate interface: def draw_ratings(layout, context, asset): @@ -63,9 +62,8 @@ def draw_ratings(layout, context, asset): # layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0) row = col.row() - row.prop(bkit_ratings , 'rating_quality_ui', expand=True, icon_only=True, emboss=False) - #ratings.draw_rating(col, bkit_ratings, 'rating_quality', 'Quality') - if bkit_ratings.rating_quality>0: + row.prop(bkit_ratings, 'rating_quality_ui', expand=True, icon_only=True, emboss=False) + if bkit_ratings.rating_quality > 0: col.separator() col.prop(bkit_ratings, 'rating_work_hours') # w = context.region.width @@ -81,7 +79,7 @@ def draw_ratings(layout, context, asset): # re-enable layout if included in longer panel -def draw_not_logged_in(source, message = 'Please Login/Signup to use this feature' ): +def draw_not_logged_in(source, message='Please Login/Signup to use this feature'): title = "You aren't logged in" def draw_message(source, context): @@ -104,7 +102,7 @@ def draw_upload_common(layout, props, asset_type, context): row = layout.row(align=True) if props.upload_state != '': - utils.label_multiline(layout, text=props.upload_state, width=context.region.width) + utils.label_multiline(layout, text=props.upload_state, width=context.region.width) if props.uploading: op = layout.operator('object.kill_bg_process', text="", icon='CANCEL') op.process_source = asset_type @@ -193,7 +191,7 @@ def draw_panel_model_upload(self, context): op.process_source = 'MODEL' op.process_type = 'THUMBNAILER' elif props.thumbnail_generating_state != '': - utils.label_multiline(layout, text=props.thumbnail_generating_state) + utils.label_multiline(layout, text=props.thumbnail_generating_state) layout.prop(props, 'description') layout.prop(props, 'tags') @@ -359,7 +357,8 @@ class VIEW3D_PT_blenderkit_model_properties(Panel): o = utils.get_active_model() # o = bpy.context.active_object if o.get('asset_data') is None: - utils.label_multiline(layout, text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') + utils.label_multiline(layout, + text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') layout.prop(o, 'name') if o.get('asset_data') is not None: @@ -383,12 +382,14 @@ class VIEW3D_PT_blenderkit_model_properties(Panel): # fun override project, not finished # layout.operator('object.blenderkit_color_corrector') -def draw_rating_asset(self,context,asset): + +def draw_rating_asset(self, context, asset): layout = self.layout col = layout.box() # split = layout.split(factor=0.5) # col1 = split.column() # col2 = split.column() + # print('%s_search' % asset['asset_data']['assetType']) directory = paths.get_temp_dir('%s_search' % asset['asset_data']['assetType']) tpath = os.path.join(directory, asset['asset_data']['thumbnail_small']) for image in bpy.data.images: @@ -401,8 +402,6 @@ def draw_rating_asset(self,context,asset): draw_ratings(col, context, asset=asset) - - class VIEW3D_PT_blenderkit_ratings(Panel): bl_category = "BlenderKit" bl_idname = "VIEW3D_PT_blenderkit_ratings" @@ -418,16 +417,17 @@ class VIEW3D_PT_blenderkit_ratings(Panel): return p def draw(self, context): - #TODO make a list of assets inside asset appending code, to happen only when assets are added to the scene. + # TODO make a list of assets inside asset appending code, to happen only when assets are added to the scene. # draw asset properties here layout = self.layout assets = ratings.get_assets_for_rating() - if len(assets)>0: - layout.label(text = 'Help BlenderKit community') - layout.label(text = 'by rating these assets:') + if len(assets) > 0: + layout.label(text='Help BlenderKit community') + layout.label(text='by rating these assets:') for a in assets: - draw_rating_asset(self, context, asset = a) + draw_rating_asset(self, context, asset=a) + def draw_login_progress(layout): layout.label(text='Login through browser') @@ -520,7 +520,7 @@ def draw_panel_model_rating(self, context): # o = bpy.context.active_object o = utils.get_active_model() # print('ratings active',o) - draw_ratings(self.layout, context, asset = o) # , props) + draw_ratings(self.layout, context, asset=o) # , props) # op.asset_type = 'MODEL' @@ -564,7 +564,7 @@ def draw_panel_material_upload(self, context): op.process_source = 'MATERIAL' op.process_type = 'THUMBNAILER' elif props.thumbnail_generating_state != '': - utils.label_multiline(layout, text=props.thumbnail_generating_state) + utils.label_multiline(layout, text=props.thumbnail_generating_state) if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'): layout.operator("object.blenderkit_material_thumbnail", text='Render thumbnail with Cycles', icon='EXPORT') @@ -634,12 +634,12 @@ def draw_panel_brush_search(self, context): def draw_panel_brush_ratings(self, context): # props = utils.get_brush_props(context) brush = utils.get_active_brush() - draw_ratings(self.layout, context, asset = brush) # , props) + draw_ratings(self.layout, context, asset=brush) # , props) # # op.asset_type = 'BRUSH' -def draw_login_buttons(layout, invoke = False): +def draw_login_buttons(layout, invoke=False): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences if user_preferences.login_attempt: @@ -782,7 +782,7 @@ class VIEW3D_PT_blenderkit_categories(Panel): s = context.scene ui_props = s.blenderkitUI mode = True - if ui_props.asset_type == 'BRUSH' and not (context.sculpt_object or context.image_paint_object): + if ui_props.asset_type == 'BRUSH' and not (context.sculpt_object or context.image_paint_object): mode = False return ui_props.down_up == 'SEARCH' and mode @@ -820,6 +820,10 @@ class VIEW3D_PT_blenderkit_import_settings(Panel): layout.prop(props, 'randomize_rotation') if props.randomize_rotation: layout.prop(props, 'randomize_rotation_amount') + layout.prop(props, 'perpendicular_snap') + if props.perpendicular_snap: + layout.prop(props,'perpendicular_snap_threshold') + if ui_props.asset_type == 'MATERIAL': props = s.blenderkit_mat layout.prop(props, 'automap') @@ -924,7 +928,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): return; if ui_props.asset_type == 'MODEL': - #utils.label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') + # utils.label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None: draw_panel_model_upload(self, context) else: @@ -933,12 +937,12 @@ class VIEW3D_PT_blenderkit_unified(Panel): draw_panel_scene_upload(self, context) elif ui_props.asset_type == 'MATERIAL': - #utils.label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') + # utils.label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: draw_panel_material_upload(self, context) else: - utils.label_multiline(layout, text='select object with material to upload materials', width=w) + utils.label_multiline(layout, text='select object with material to upload materials', width=w) elif ui_props.asset_type == 'BRUSH': if context.sculpt_object or context.image_paint_object: @@ -972,6 +976,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): if ui_props.asset_type == 'TEXTURE': layout.label(text='not yet implemented') + class BlenderKitWelcomeOperator(bpy.types.Operator): """Login online on BlenderKit webpage""" @@ -993,27 +998,36 @@ class BlenderKitWelcomeOperator(bpy.types.Operator): def draw(self, context): layout = self.layout if self.step == 0: - message = "BlenderKit is an addon that connects to the internet to search and upload for models, materials, and brushes. \n\n Let's start by searching for some cool materials?" + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + + message = "BlenderKit connects from Blender to an online, " \ + "community built shared library of models, " \ + "materials, and brushes. " \ + "Use addon preferences to set up where files will be saved in the Global directory setting." + + utils.label_multiline(layout, text=message, width=300) + utils.label_multiline(layout, text="\n Let's start by searching for some cool materials?", width=300) else: - message = "This shouldn't be here at all" - utils.label_multiline(layout, text= message, width = 300) + message = "Operator Tutorial called with invalid step" def execute(self, context): if self.step == 0: - #move mouse: - #bpy.context.window_manager.windows[0].cursor_warp(1000, 1000) - #show n-key sidebar (spaces[index] has to be found for view3d too: + # move mouse: + # bpy.context.window_manager.windows[0].cursor_warp(1000, 1000) + # show n-key sidebar (spaces[index] has to be found for view3d too: # bpy.context.window_manager.windows[0].screen.areas[5].spaces[0].show_region_ui = False print('running search no') ui_props = bpy.context.scene.blenderkitUI ui_props.asset_type = 'MATERIAL' - search.search() + bpy.context.scene.blenderkit_mat.search_keywords = 'ice' + # search.search() return {'FINISHED'} def invoke(self, context, event): wm = bpy.context.window_manager return wm.invoke_props_dialog(self) + def draw_asset_context_menu(self, context, asset_data): layout = self.layout ui_props = context.scene.blenderkitUI @@ -1024,10 +1038,11 @@ def draw_asset_context_menu(self, context, asset_data): a = bpy.context.window_manager['bkit authors'].get(author_id) if a is not None: # utils.p('author:', a) + op = layout.operator('wm.url_open', text="Open Author's Website") if a.get('aboutMeUrl') is not None: - op = layout.operator('wm.url_open', text="Open Author's Website") op.url = a['aboutMeUrl'] - + else: + op.url = paths.get_author_gallery_url(a['id']) op = layout.operator('view3d.blenderkit_search', text="Show Assets By Author") op.keywords = '' op.author_id = author_id @@ -1080,6 +1095,8 @@ def draw_asset_context_menu(self, context, asset_data): op.asset_id = asset_data['id'] op.state = 'rejected' + + if author_id == str(profile['user']['id']): layout.label(text='Management tools:') row = layout.row() @@ -1087,9 +1104,13 @@ def draw_asset_context_menu(self, context, asset_data): op = row.operator('object.blenderkit_change_status', text='Delete') op.asset_id = asset_data['id'] op.state = 'deleted' - # else: - # #not an author - can rate - # draw_ratings(layout, context) + + if utils.profile_is_validator(): + layout.label(text='Admin rating Tools:') + + op = layout.operator('wm.blenderkit_menu_rating_upload', text='Fast rate') + op.asset_id = asset_data['id'] + op.asset_type = asset_data['assetType'] class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): @@ -1104,6 +1125,7 @@ class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): asset_data = sr[ui_props.active_index] draw_asset_context_menu(self, context, asset_data) + class OBJECT_MT_blenderkit_login_menu(bpy.types.Menu): bl_label = "BlenderKit login/signup:" bl_idname = "OBJECT_MT_blenderkit_login_menu" @@ -1188,6 +1210,7 @@ class UrlPopupDialog(bpy.types.Operator): return wm.invoke_props_dialog(self) + class LoginPopupDialog(bpy.types.Operator): """Generate Cycles thumbnail for model assets""" bl_idname = "wm.blenderkit_url_dialog" diff --git a/blenderkit/upload.py b/blenderkit/upload.py index 18e43b8a..14fbe6db 100644 --- a/blenderkit/upload.py +++ b/blenderkit/upload.py @@ -767,7 +767,7 @@ class UploadOperator(Operator): layout.label(text="For updates of thumbnail or model use reupload.") if props.is_private == 'PUBLIC': - ui_panels.label_multiline(layout, text='public assets are validated several hours' + utils.label_multiline(layout, text='public assets are validated several hours' ' or days after upload. Remember always to ' 'test download your asset to a clean file' ' to see if it uploaded correctly.' -- cgit v1.2.3 From 497f422df0a3165c01c4db1acff78fbad4c25d11 Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Mon, 27 Jul 2020 15:47:45 -0400 Subject: Community Themes: Update Deep Grey @TheRedWaxPolice provided this update that makes checkbox outlines slightly dimmer. --- presets/interface_theme/deep_grey.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/presets/interface_theme/deep_grey.xml b/presets/interface_theme/deep_grey.xml index b6ddb751..951b4875 100644 --- a/presets/interface_theme/deep_grey.xml +++ b/presets/interface_theme/deep_grey.xml @@ -107,7 +107,7 @@ - + \ No newline at end of file -- cgit v1.2.3 From f156a1248b974454e0cc20b8f0e04b1434d6293b Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 28 Jul 2020 11:38:53 +0200 Subject: Bone Selection Sets: Make all property definitions Library Overridable Without this, the addon simply doesn't work on library overridden rigs, since all the addon's interface is grayed out with an error message saying the properties are not overridable. Reviewed By: mont29 Differential Revision: https://developer.blender.org/D8403 --- bone_selection_sets.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bone_selection_sets.py b/bone_selection_sets.py index 8e07f418..86338da2 100644 --- a/bone_selection_sets.py +++ b/bone_selection_sets.py @@ -50,13 +50,13 @@ from bpy.props import ( # Note: bones are stored by name, this means that if the bone is renamed, # there can be problems. However, bone renaming is unlikely during animation. class SelectionEntry(PropertyGroup): - name: StringProperty(name="Bone Name") + name: StringProperty(name="Bone Name", override={'LIBRARY_OVERRIDABLE'}) class SelectionSet(PropertyGroup): - name: StringProperty(name="Set Name") - bone_ids: CollectionProperty(type=SelectionEntry) - is_selected: BoolProperty(name="Is Selected") + name: StringProperty(name="Set Name", override={'LIBRARY_OVERRIDABLE'}) + bone_ids: CollectionProperty(type=SelectionEntry, override={'LIBRARY_OVERRIDABLE'}) + is_selected: BoolProperty(name="Is Selected", override={'LIBRARY_OVERRIDABLE'}) # UI Panel w/ UIList ########################################################## @@ -545,12 +545,14 @@ def register(): bpy.types.Object.selection_sets = CollectionProperty( type=SelectionSet, name="Selection Sets", - description="List of groups of bones for easy selection" + description="List of groups of bones for easy selection", + override={'LIBRARY_OVERRIDABLE'} ) bpy.types.Object.active_selection_set = IntProperty( name="Active Selection Set", description="Index of the currently active selection set", - default=0 + default=0, + override={'LIBRARY_OVERRIDABLE'} ) # Add shortcuts to the keymap. -- cgit v1.2.3 From e1dae55cca702ef4a140a455d88099d666230c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Tue, 28 Jul 2020 15:31:28 +0200 Subject: BlenderKit: fix data update -older fines could act as broken --- blenderkit/append_link.py | 8 ++++++++ blenderkit/download.py | 20 +++++++++++++++++--- blenderkit/search.py | 40 +++++++++++++++++++++++++++++----------- blenderkit/ui.py | 5 +++-- blenderkit/ui_panels.py | 7 ++++++- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/blenderkit/append_link.py b/blenderkit/append_link.py index b6bfb791..2301daac 100644 --- a/blenderkit/append_link.py +++ b/blenderkit/append_link.py @@ -88,6 +88,8 @@ def append_scene(file_name, scenename=None, link=False, fake_user=False): def link_collection(file_name, obnames=[], location=(0, 0, 0), link=False, parent = None, **kwargs): '''link an instanced group - model type asset''' sel = utils.selection_get() + print('link collection') + print(kwargs) with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): scols = [] @@ -115,6 +117,12 @@ def link_collection(file_name, obnames=[], location=(0, 0, 0), link=False, paren main_object.instance_collection = col break; + #sometimes, the lib might already be without the actual link. + if not main_object.instance_collection and kwargs['name']: + col = bpy.data.collections.get(kwargs['name']) + if col: + main_object.instance_collection = col + main_object.name = main_object.instance_collection.name # bpy.ops.wm.link(directory=file_name + "/Collection/", filename=kwargs['name'], link=link, instance_collections=True, diff --git a/blenderkit/download.py b/blenderkit/download.py index 7ec425ce..c14fc84d 100644 --- a/blenderkit/download.py +++ b/blenderkit/download.py @@ -343,6 +343,13 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, parent=kwargs.get('parent')) else: + # parent, newobs = append_link.link_collection(file_names[-1], + # location=downloader['location'], + # rotation=downloader['rotation'], + # link=False, + # name=asset_data['name'], + # parent=kwargs.get('parent')) + parent, newobs = append_link.append_objects(file_names[-1], location=downloader['location'], rotation=downloader['rotation'], @@ -364,10 +371,17 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, name=asset_data['name'], parent=kwargs.get('parent')) else: + # parent, newobs = append_link.link_collection(file_names[-1], + # location=kwargs['model_location'], + # rotation=kwargs['model_rotation'], + # link=False, + # name=asset_data['name'], + # parent=kwargs.get('parent')) parent, newobs = append_link.append_objects(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], link=link, + name=asset_data['name'], parent=kwargs.get('parent')) if parent.type == 'EMPTY' and link: bmin = asset_data['bbox_min'] @@ -723,8 +737,8 @@ def try_finished_append(asset_data, **kwargs): # location=None, material_target for f in file_names: try: os.remove(f) - except: - e = sys.exc_info()[0] + except Exception as e: + # e = sys.exc_info()[0] print(e) pass; done = False @@ -934,7 +948,7 @@ class BlenderkitDownloadOperator(bpy.types.Operator): if self.replace: # cleanup first, assign later. obs = utils.get_selected_replace_adepts() - print(obs) + # print(obs) for ob in obs: print('replace attempt ', ob.name) if self.asset_base_id != '': diff --git a/blenderkit/search.py b/blenderkit/search.py index 6c8d16e2..82bb8f58 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -101,22 +101,40 @@ def refresh_token_timer(): return max(3600, user_preferences.api_key_life - 3600) +def update_ad(ad): + if not ad.get('assetBaseId'): + ad['assetBaseId'] = ad['asset_base_id'] # this should stay ONLY for compatibility with older scenes + ad['assetType'] = ad['asset_type'] # this should stay ONLY for compatibility with older scenes + ad['canDownload'] = ad['can_download'] # this should stay ONLY for compatibility with older scenes + ad['verificationStatus'] = ad['verification_status'] # this should stay ONLY for compatibility with older scenes + ad['author'] = {} + ad['author']['id'] = ad['author_id'] # this should stay ONLY for compatibility with older scenes + return ad def update_assets_data(): # updates assets data on scene load. '''updates some properties that were changed on scenes with older assets. The properties were mainly changed from snake_case to CamelCase to fit the data that is coming from the server. ''' - for ob in bpy.context.scene.objects: - if ob.get('asset_data') != None: - ad = ob['asset_data'] - if not ad.get('assetBaseId'): - ad['assetBaseId'] = ad['asset_base_id'], # this should stay ONLY for compatibility with older scenes - ad['assetType'] = ad['asset_type'], # this should stay ONLY for compatibility with older scenes - ad['canDownload'] = ad['can_download'], # this should stay ONLY for compatibility with older scenes - ad['verificationStatus'] = ad[ - 'verification_status'], # this should stay ONLY for compatibility with older scenes - ad['author'] = {} - ad['author']['id'] = ad['author_id'], # this should stay ONLY for compatibility with older scenes + data = bpy.data + + datablocks = [ + bpy.data.objects, + bpy.data.materials, + bpy.data.brushes, + ] + for dtype in datablocks: + for block in dtype: + if block.get('asset_data') != None: + update_ad(block['asset_data']) + + dicts = [ + 'assets used', + 'assets rated', + ] + for d in dicts: + for k in d.keys(): + update_ad(d[k]) + # bpy.context.scene['assets used'][ad] = ad @persistent diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 7935363d..80b7ef2c 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1033,9 +1033,10 @@ def is_rating_possible(): m = ao.active_material if m is not None: ad = m.get('asset_data') - if ad is not None: + if ad is not None and ad.get('assetBaseId'): rated = bpy.context.scene['assets rated'].get(ad['assetBaseId']) - return True, rated, m, ad + if rated: + return True, rated, m, ad # if t>2 and t<2.5: # ui_props.rating_on = False diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index 89646862..a591e76e 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -1048,7 +1048,12 @@ def draw_asset_context_menu(self, context, asset_data): op.author_id = author_id op = layout.operator('view3d.blenderkit_search', text='Search Similar') - op.keywords = asset_data['name'] + ' ' + asset_data['description'] + ' ' + ' '.join(asset_data['tags']) + #build search string from description and tags: + op.keywords = asset_data['name'] + if asset_data.get('description'): + op.keywords += ' ' + asset_data.get('description') + op.keywords += ' '.join(asset_data.get('tags')) + if asset_data.get('canDownload') != 0: if len(bpy.context.selected_objects) > 0 and ui_props.asset_type == 'MODEL': aob = bpy.context.active_object -- cgit v1.2.3 From c6a7bba3f0f74d7f90195cafa1456f91f9b9f521 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 28 Jul 2020 17:57:26 +0300 Subject: Rigify: remove an extraneous 'self' reference. --- rigify/utils/bones.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rigify/utils/bones.py b/rigify/utils/bones.py index 51a4d44c..f0341a8f 100644 --- a/rigify/utils/bones.py +++ b/rigify/utils/bones.py @@ -382,7 +382,7 @@ class BoneUtilityMixin(object): def new_bone(self, new_name): """Create a new bone with the specified name.""" name = new_bone(self.obj, new_name) - self.register_new_bone(self, name) + self.register_new_bone(name) return name def copy_bone(self, bone_name, new_name='', *, parent=False, bbone=False, length=None, scale=None): -- cgit v1.2.3 From 3bdb5f41aea4897762edd09a71b0bfa8b0b7bc10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 29 Jul 2020 20:11:17 +0200 Subject: BlenderKit: fix appending -this was broken due to API changes. Also no need for so much magic now since the default append just works well. -fix a bug in previous commit (asset update) --- blenderkit/append_link.py | 39 ++++++++++++++++++++++++++++++++++++--- blenderkit/download.py | 32 +++++++++++--------------------- blenderkit/search.py | 13 +++++++++---- blenderkit/ui.py | 2 +- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/blenderkit/append_link.py b/blenderkit/append_link.py index 2301daac..5af63fe1 100644 --- a/blenderkit/append_link.py +++ b/blenderkit/append_link.py @@ -189,12 +189,44 @@ def append_particle_system(file_name, obnames=[], location=(0, 0, 0), link=False def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs): '''append objects into scene individually''' + #simplified version of append + scene = bpy.context.scene + sel = utils.selection_get() + bpy.ops.object.select_all(action='DESELECT') + + path = file_name + "\\Collection\\" + object_name = kwargs.get('name') + fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D') + bpy.ops.wm.append(fc,filename=object_name, directory=path) + + + return_obs = [] + for ob in bpy.context.scene.objects: + if ob.select_get(): + return_obs.append(ob) + if not ob.parent: + main_object = ob + ob.location = location + + if kwargs.get('rotation'): + main_object.rotation_euler = kwargs['rotation'] + + if kwargs.get('parent') is not None: + main_object.parent = bpy.data.objects[kwargs['parent']] + main_object.matrix_world.translation = location + + bpy.ops.object.select_all(action='DESELECT') + utils.selection_set(sel) + + return main_object, return_obs with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): sobs = [] - for ob in data_from.objects: - if ob in obnames or obnames == []: - sobs.append(ob) + for col in data_from.collections: + if col == kwargs.get('name'): + for ob in col.objects: + if ob in obnames or obnames == []: + sobs.append(ob) data_to.objects = sobs # data_to.objects = data_from.objects#[name for name in data_from.objects if name.startswith("house")] @@ -221,6 +253,7 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar hidden_objects.append(obj) obj.hide_viewport = False return_obs.append(obj) + # Only after all objects are in scene! Otherwise gets broken relationships if link == True: bpy.ops.object.make_local(type='SELECT_OBJECT') diff --git a/blenderkit/download.py b/blenderkit/download.py index c14fc84d..a93611c0 100644 --- a/blenderkit/download.py +++ b/blenderkit/download.py @@ -307,17 +307,15 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, # copy for override al = sprops.append_link - import_as = sprops.import_as # set consistency for objects already in scene, otherwise this literally breaks blender :) ain = asset_in_scene(asset_data) + # override based on history if ain is not False: if ain == 'LINKED': al = 'LINK' - import_as = 'GROUP' else: al = 'APPEND' - import_as = 'INDIVIDUAL' # first get conditions for append link link = al == 'LINK' @@ -334,7 +332,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, name=asset_data['name']) return - if sprops.import_as == 'GROUP': + if link: parent, newobs = append_link.link_collection(file_names[-1], location=downloader['location'], rotation=downloader['rotation'], @@ -343,12 +341,6 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, parent=kwargs.get('parent')) else: - # parent, newobs = append_link.link_collection(file_names[-1], - # location=downloader['location'], - # rotation=downloader['rotation'], - # link=False, - # name=asset_data['name'], - # parent=kwargs.get('parent')) parent, newobs = append_link.append_objects(file_names[-1], location=downloader['location'], @@ -363,7 +355,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, parent.empty_display_size = size_min elif kwargs.get('model_location') is not None: - if sprops.import_as == 'GROUP': + if link: parent, newobs = append_link.link_collection(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], @@ -371,18 +363,14 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, name=asset_data['name'], parent=kwargs.get('parent')) else: - # parent, newobs = append_link.link_collection(file_names[-1], - # location=kwargs['model_location'], - # rotation=kwargs['model_rotation'], - # link=False, - # name=asset_data['name'], - # parent=kwargs.get('parent')) parent, newobs = append_link.append_objects(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], link=link, name=asset_data['name'], parent=kwargs.get('parent')) + + #scale Empty for assets, so they don't clutter the scene. if parent.type == 'EMPTY' and link: bmin = asset_data['bbox_min'] bmax = asset_data['bbox_max'] @@ -758,10 +746,12 @@ def asset_in_scene(asset_data): asset_data['file_name'] = ad['file_name'] asset_data['url'] = ad['url'] - c = bpy.data.collections.get(ad['name']) - if c is not None: - if c.users > 0: - return 'LINKED' + #browse all collections since linked collections can have same name. + for c in bpy.data.collections: + if c.name == ad['name']: + #there can also be more linked collections with same name, we need to check id. + if c.library and c.library.get('asset_data') and c.library['asset_data']['assetBaseId'] == id: + return 'LINKED' return 'APPENDED' return False diff --git a/blenderkit/search.py b/blenderkit/search.py index 82bb8f58..25d85d88 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -131,10 +131,15 @@ def update_assets_data(): # updates assets data on scene load. 'assets used', 'assets rated', ] - for d in dicts: - for k in d.keys(): - update_ad(d[k]) - # bpy.context.scene['assets used'][ad] = ad + for s in bpy.data.scenes: + for k in dicts: + d = s.get(k) + if not d: + continue; + + for k in d.keys(): + update_ad(d[k]) + # bpy.context.scene['assets used'][ad] = ad @persistent diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 80b7ef2c..7ae6988a 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -738,7 +738,7 @@ def draw_callback_2d_search(self, context): highlight = (1, 1, 1, .2) # highlight = (1, 1, 1, 0.8) # background of asset bar - if not ui_props.dragging: + if not ui_props.dragging and ui_props.hcount>0: search_results = s.get('search results') search_results_orig = s.get('search results orig') if search_results == None: -- cgit v1.2.3 From a3a1815d36afbccbd45b52c91afc4e543d4154df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 15 Jul 2020 00:39:45 +0200 Subject: BlenderKit: fix login after token refresh fails. Now offers a popup to login on site, previously only reported about invalid token, which wasn't clear to many users. (cherry picked from commit c52cfd99ff31f7554cc998c69382d1c8dd7ed8ed) --- blenderkit/bkit_oauth.py | 4 ++-- blenderkit/rerequests.py | 5 +++++ blenderkit/search.py | 4 ++-- blenderkit/tasks_queue.py | 13 +++++++++---- blenderkit/ui.py | 34 +++------------------------------- blenderkit/utils.py | 33 +++++++++++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 39 deletions(-) diff --git a/blenderkit/bkit_oauth.py b/blenderkit/bkit_oauth.py index ae90b215..59ed6c8b 100644 --- a/blenderkit/bkit_oauth.py +++ b/blenderkit/bkit_oauth.py @@ -116,7 +116,7 @@ class RegisterLoginOnline(bpy.types.Operator): message: bpy.props.StringProperty( name="Message", description="", - default="You were logged out from BlenderKit. Clicking OK takes you to web login. ") + default="You were logged out from BlenderKit.\n Clicking OK takes you to web login. ") @classmethod def poll(cls, context): @@ -124,7 +124,7 @@ class RegisterLoginOnline(bpy.types.Operator): def draw(self, context): layout = self.layout - utils.label_multiline(layout, text=self.message) + utils.label_multiline(layout, text=self.message, width = 300) def execute(self, context): preferences = bpy.context.preferences.addons['blenderkit'].preferences diff --git a/blenderkit/rerequests.py b/blenderkit/rerequests.py index 3d9a4d75..c655c8c5 100644 --- a/blenderkit/rerequests.py +++ b/blenderkit/rerequests.py @@ -76,6 +76,11 @@ def rerequest(method, url, **kwargs): utils.p('reresult', response.status_code) if response.status_code >= 400: utils.p('reresult', response.text) + else: + tasks_queue.add_task((ui.add_report, ( + 'Refreshing token failed.Please login manually.', 10))) + # tasks_queue.add_task((bkit_oauth.write_tokens, ('', '', ''))) + tasks_queue.add_task((bpy.ops.wm.blenderkit_login,( 'INVOKE_DEFAULT',)),fake_context = True) return response diff --git a/blenderkit/search.py b/blenderkit/search.py index 09dfeb65..f6226049 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -72,7 +72,7 @@ def check_errors(rdata): if user_preferences.enable_oauth: bkit_oauth.refresh_token_thread() return False, rdata.get('detail') - return False, 'Missing or wrong api_key in addon preferences' + return False, 'Use login panel to connect your profile.' return True, '' @@ -282,7 +282,7 @@ def timer_update(): search() preferences.first_run = False if preferences.tips_on_start: - ui.get_largest_3dview() + utils.get_largest_3dview() ui.update_ui_size(ui.active_area, ui.active_region) ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN) return 3.0 diff --git a/blenderkit/tasks_queue.py b/blenderkit/tasks_queue.py index bbac6d63..a253aa96 100644 --- a/blenderkit/tasks_queue.py +++ b/blenderkit/tasks_queue.py @@ -45,15 +45,16 @@ def get_queue(): return t.task_queue class task_object: - def __init__(self, command = '', arguments = (), wait = 0, only_last = False): + def __init__(self, command = '', arguments = (), wait = 0, only_last = False, fake_context = False): self.command = command self.arguments = arguments self.wait = wait self.only_last = only_last + self.fake_context = fake_context -def add_task(task, wait = 0, only_last = False): +def add_task(task, wait = 0, only_last = False, fake_context = False): q = get_queue() - taskob = task_object(task[0],task[1], wait = wait, only_last = only_last) + taskob = task_object(task[0],task[1], wait = wait, only_last = only_last, fake_context = fake_context) q.put(taskob) @@ -90,7 +91,11 @@ def queue_worker(): utils.p('as a task: ') utils.p(task.command, task.arguments) try: - task.command(*task.arguments) + if task.fake_context: + fc = utils.get_fake_context(bpy.context) + task.command(fc,*task.arguments) + else: + task.command(*task.arguments) except Exception as e: utils.p('task failed:') print(e) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index a1cd66d9..fa26d8a3 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1181,30 +1181,6 @@ def update_ui_size(area, region): ui.rating_y = ui.bar_y - ui.bar_height -def get_largest_3dview(): - maxsurf = 0 - maxa = None - maxw = None - region = None - for w in bpy.context.window_manager.windows: - screen = w.screen - for a in screen.areas: - if a.type == 'VIEW_3D': - asurf = a.width * a.height - if asurf > maxsurf: - maxa = a - maxw = w - maxsurf = asurf - - for r in a.regions: - if r.type == 'WINDOW': - region = r - global active_area, active_window, active_region - active_window = maxw - active_area = maxa - active_region = region - return maxw, maxa, region - class AssetBarOperator(bpy.types.Operator): '''runs search and displays the asset bar at the same time''' @@ -1808,13 +1784,14 @@ class UndoWithContext(bpy.types.Operator): C_dict = bpy.context.copy() C_dict.update(region='WINDOW') if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = get_largest_3dview() + w, a, r = utils.get_largest_3dview() override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) bpy.ops.ed.undo_push(C_dict, 'INVOKE_REGION_WIN', message=self.message) return {'FINISHED'} + class RunAssetBarWithContext(bpy.types.Operator): """Regenerate cobweb""" bl_idname = "object.run_assetbar_fix_context" @@ -1826,12 +1803,7 @@ class RunAssetBarWithContext(bpy.types.Operator): # return {'RUNNING_MODAL'} def execute(self, context): - C_dict = bpy.context.copy() - C_dict.update(region='WINDOW') - if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = get_largest_3dview() - override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} - C_dict.update(override) + C_dict = utils.get_fake_context(context) bpy.ops.view3d.blenderkit_asset_bar(C_dict, 'INVOKE_REGION_WIN', keep_running=True, do_search=False) return {'FINISHED'} diff --git a/blenderkit/utils.py b/blenderkit/utils.py index 2e59887c..78eff216 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -614,6 +614,39 @@ def guard_from_crash(): return True +def get_largest_3dview(): + maxsurf = 0 + maxa = None + maxw = None + region = None + for w in bpy.context.window_manager.windows: + screen = w.screen + for a in screen.areas: + if a.type == 'VIEW_3D': + asurf = a.width * a.height + if asurf > maxsurf: + maxa = a + maxw = w + maxsurf = asurf + + for r in a.regions: + if r.type == 'WINDOW': + region = r + global active_area, active_window, active_region + active_window = maxw + active_area = maxa + active_region = region + return maxw, maxa, region + +def get_fake_context(context): + C_dict = context.copy() + C_dict.update(region='WINDOW') + if context.area is None or context.area.type != 'VIEW_3D': + w, a, r = get_largest_3dview() + override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} + C_dict.update(override) + return C_dict + def label_multiline(layout, text='', icon='NONE', width=-1): ''' draw a ui label, but try to split it in multiple lines.''' if text.strip() == '': -- cgit v1.2.3 From 66bd6dea71862d8f55ede79879117d9e7de882a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 15 Jul 2020 19:06:53 +0200 Subject: BlenderKit: on-registration popup This popup informs the user that BlenderKit connects to the internet directly after registration, and asks for consent with it and also performs first search. (cherry picked from commit 00fefe2d147288e3a218d640668b54331e82d3e8) --- blenderkit/__init__.py | 5 +++++ blenderkit/search.py | 8 +++---- blenderkit/tasks_queue.py | 9 ++++---- blenderkit/ui.py | 2 +- blenderkit/ui_panels.py | 56 +++++++++++++++++++++++++++++++++++++++++++++-- blenderkit/utils.py | 16 ++++++++------ 6 files changed, 78 insertions(+), 18 deletions(-) diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index a7e148ea..23ee89c5 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -1725,6 +1725,11 @@ def register(): bpy.app.timers.register(check_timers_timer, persistent=True) bpy.app.handlers.load_post.append(scene_load) + # detect if the user just enabled the addon in preferences, thus enable to run + for w in bpy.context.window_manager.windows: + for a in w.screen.areas: + if a.type == 'PREFERENCES': + tasks_queue.add_task((bpy.ops.wm.blenderkit_welcome,( 'INVOKE_DEFAULT',)),fake_context = True, fake_context_area = 'PREFERENCES') def unregister(): diff --git a/blenderkit/search.py b/blenderkit/search.py index f6226049..cee0b14b 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -282,14 +282,14 @@ def timer_update(): search() preferences.first_run = False if preferences.tips_on_start: - utils.get_largest_3dview() + utils.get_largest_area() ui.update_ui_size(ui.active_area, ui.active_region) ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN) return 3.0 - if preferences.first_run: - search() - preferences.first_run = False + # if preferences.first_run: + # search() + # preferences.first_run = False # check_clipboard() diff --git a/blenderkit/tasks_queue.py b/blenderkit/tasks_queue.py index a253aa96..5a327290 100644 --- a/blenderkit/tasks_queue.py +++ b/blenderkit/tasks_queue.py @@ -45,16 +45,17 @@ def get_queue(): return t.task_queue class task_object: - def __init__(self, command = '', arguments = (), wait = 0, only_last = False, fake_context = False): + def __init__(self, command = '', arguments = (), wait = 0, only_last = False, fake_context = False, fake_context_area = 'VIEW_3D'): self.command = command self.arguments = arguments self.wait = wait self.only_last = only_last self.fake_context = fake_context + self.fake_context_area = fake_context_area -def add_task(task, wait = 0, only_last = False, fake_context = False): +def add_task(task, wait = 0, only_last = False, fake_context = False, fake_context_area = 'VIEW_3D'): q = get_queue() - taskob = task_object(task[0],task[1], wait = wait, only_last = only_last, fake_context = fake_context) + taskob = task_object(task[0],task[1], wait = wait, only_last = only_last, fake_context = fake_context, fake_context_area = fake_context_area) q.put(taskob) @@ -92,7 +93,7 @@ def queue_worker(): utils.p(task.command, task.arguments) try: if task.fake_context: - fc = utils.get_fake_context(bpy.context) + fc = utils.get_fake_context(bpy.context, area_type = task.fake_context_area) task.command(fc,*task.arguments) else: task.command(*task.arguments) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index fa26d8a3..30195168 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1784,7 +1784,7 @@ class UndoWithContext(bpy.types.Operator): C_dict = bpy.context.copy() C_dict.update(region='WINDOW') if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = utils.get_largest_3dview() + w, a, r = utils.get_largest_area() override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) bpy.ops.ed.undo_push(C_dict, 'INVOKE_REGION_WIN', message=self.message) diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index b8d8ce73..d067afa0 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -25,12 +25,22 @@ if "bpy" in locals(): download = importlib.reload(download) categories = importlib.reload(categories) icons = importlib.reload(icons) + icons = importlib.reload(search) else: - from blenderkit import paths, ratings, utils, download, categories, icons + from blenderkit import paths, ratings, utils, download, categories, icons, search from bpy.types import ( Panel ) +from bpy.props import ( + IntProperty, + FloatProperty, + FloatVectorProperty, + StringProperty, + EnumProperty, + BoolProperty, + PointerProperty, +) import bpy import os @@ -962,6 +972,47 @@ class VIEW3D_PT_blenderkit_unified(Panel): if ui_props.asset_type == 'TEXTURE': layout.label(text='not yet implemented') +class BlenderKitWelcomeOperator(bpy.types.Operator): + """Login online on BlenderKit webpage""" + + bl_idname = "wm.blenderkit_welcome" + bl_label = "Welcome to BlenderKit!" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + step: IntProperty( + name="step", + description="Tutorial Step", + default=0, + options={'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + if self.step == 0: + message = "BlenderKit is an addon that connects to the internet to search and upload for models, materials, and brushes. \n\n Let's start by searching for some cool materials?" + else: + message = "This shouldn't be here at all" + utils.label_multiline(layout, text= message, width = 300) + + def execute(self, context): + if self.step == 0: + #move mouse: + #bpy.context.window_manager.windows[0].cursor_warp(1000, 1000) + #show n-key sidebar (spaces[index] has to be found for view3d too: + # bpy.context.window_manager.windows[0].screen.areas[5].spaces[0].show_region_ui = False + print('running search no') + ui_props = bpy.context.scene.blenderkitUI + ui_props.asset_type = 'MATERIAL' + search.search() + return {'FINISHED'} + + def invoke(self, context, event): + wm = bpy.context.window_manager + return wm.invoke_props_dialog(self) def draw_asset_context_menu(self, context, asset_data): layout = self.layout @@ -1298,7 +1349,8 @@ classess = ( VIEW3D_PT_blenderkit_downloads, OBJECT_MT_blenderkit_asset_menu, OBJECT_MT_blenderkit_login_menu, - UrlPopupDialog + UrlPopupDialog, + BlenderKitWelcomeOperator, ) diff --git a/blenderkit/utils.py b/blenderkit/utils.py index 78eff216..effc2627 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -614,15 +614,14 @@ def guard_from_crash(): return True -def get_largest_3dview(): +def get_largest_area( area_type = 'VIEW_3D'): maxsurf = 0 maxa = None maxw = None region = None for w in bpy.context.window_manager.windows: - screen = w.screen - for a in screen.areas: - if a.type == 'VIEW_3D': + for a in w.screen.areas: + if a.type == area_type: asurf = a.width * a.height if asurf > maxsurf: maxa = a @@ -638,15 +637,18 @@ def get_largest_3dview(): active_region = region return maxw, maxa, region -def get_fake_context(context): +def get_fake_context(context, area_type = 'VIEW_3D'): C_dict = context.copy() C_dict.update(region='WINDOW') - if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = get_largest_3dview() + if context.area is None or context.area.type != area_type: + w, a, r = get_largest_area(area_type = area_type) + override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) + # print(w,a,r) return C_dict + def label_multiline(layout, text='', icon='NONE', width=-1): ''' draw a ui label, but try to split it in multiple lines.''' if text.strip() == '': -- cgit v1.2.3 From 06181ee9947d558d5323471c9b5d9792267fdb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Mon, 27 Jul 2020 12:59:38 +0200 Subject: BlenderKit: Rating refactorings This mainly paves a way for removing the old and clumsy bgl UI and enable faster rating for users. -Rating Ui is now more responsive -it can be dragged over the stars widget. -fast rating operator (f shortcut over assetbar) -wip on new ratings panel(disabled by now) -if author didn't provide his webpage, the link now leads to his profile on the BlenderKit site. -upload was partially broken thanks to a small bug -perpendicular snap - This limits angled snapping in a reasonable way, should help when placing foliage or items on sloped ceilings e.t.c. -removed the first_run property, it's replaced with a poput that informs the user about connecting to the internet. (cherry picked from commit 8f6903bc92531aa8e5d4c64a0a108c2904506a83) --- blenderkit/__init__.py | 64 +++++------- blenderkit/paths.py | 3 + blenderkit/ratings.py | 269 ++++++++++++++++++++++++++++++++++++++---------- blenderkit/search.py | 10 +- blenderkit/ui.py | 28 +++-- blenderkit/ui_panels.py | 95 ++++++++++------- blenderkit/upload.py | 2 +- 7 files changed, 328 insertions(+), 143 deletions(-) diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py index 23ee89c5..fba80a7e 100644 --- a/blenderkit/__init__.py +++ b/blenderkit/__init__.py @@ -250,6 +250,7 @@ def switch_search_results(self, context): s['search results orig'] = s.get('bkit brush search orig') search.load_previews() + def asset_type_callback(self, context): ''' Returns @@ -650,29 +651,6 @@ class BlenderKitCommonUploadProps(object): ) -def stars_enum_callback(self, context): - items = [] - for a in range(0, 10): - if self.rating_quality < a+1: - icon = 'SOLO_OFF' - else: - icon = 'SOLO_ON' - # has to have something before the number in the value, otherwise fails on registration. - items.append((f'{a+1}', f'{a+1}', '', icon, a+1)) - return items - - -def update_quality(self, context): - user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - if user_preferences.api_key == '': - # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') - # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') - # return - bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', message = 'Please login/signup to rate assets. Clicking OK takes you to web login.') - self.rating_quality_ui = '0' - self.rating_quality = int(self.rating_quality_ui) - - class BlenderKitRatingProps(PropertyGroup): rating_quality: IntProperty(name="Quality", description="quality of the material", @@ -680,19 +658,20 @@ class BlenderKitRatingProps(PropertyGroup): min=-1, max=10, update=ratings.update_ratings_quality) - #the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. + # the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. rating_quality_ui: EnumProperty(name='rating_quality_ui', - items=stars_enum_callback, - description='Rating stars 0 - 10', - default=None, - update=update_quality, - ) + items=ratings.stars_enum_callback, + description='Rating stars 0 - 10', + default=None, + update=ratings.update_quality_ui, + ) rating_work_hours: FloatProperty(name="Work Hours", description="How many hours did this work take?", default=0.00, min=0.0, max=1000, update=ratings.update_ratings_work_hours ) + # rating_complexity: IntProperty(name="Complexity", # description="Complexity is a number estimating how much work was spent on the asset.aaa", # default=0, min=0, max=10) @@ -1393,6 +1372,17 @@ class BlenderKitModelSearchProps(PropertyGroup, BlenderKitCommonSearchProps): max=180, subtype='ANGLE') + perpendicular_snap: BoolProperty(name='Perpendicular snap', + description="Limit snapping that is close to perpendicular angles to be perpendicular.", + default=True) + + perpendicular_snap_threshold: FloatProperty(name="Threshold", + description="Limit perpendicular snap to be below these values.", + default=.25, + min=0, + max=.5, + ) + class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps): search_keywords: StringProperty( @@ -1586,12 +1576,13 @@ class BlenderKitAddonPreferences(AddonPreferences): min=0, max=20000) - first_run: BoolProperty( - name="First run", - description="Detects if addon was already registered/run.", - default=True, - update=utils.save_prefs - ) + # this is now made obsolete by the new popup upon registration -ensures the user knows about the first search. + # first_run: BoolProperty( + # name="First run", + # description="Detects if addon was already registered/run.", + # default=True, + # update=utils.save_prefs + # ) use_timers: BoolProperty( name="Use timers", @@ -1729,7 +1720,8 @@ def register(): for w in bpy.context.window_manager.windows: for a in w.screen.areas: if a.type == 'PREFERENCES': - tasks_queue.add_task((bpy.ops.wm.blenderkit_welcome,( 'INVOKE_DEFAULT',)),fake_context = True, fake_context_area = 'PREFERENCES') + tasks_queue.add_task((bpy.ops.wm.blenderkit_welcome, ('INVOKE_DEFAULT',)), fake_context=True, + fake_context_area='PREFERENCES') def unregister(): diff --git a/blenderkit/paths.py b/blenderkit/paths.py index b4210a85..399e7555 100644 --- a/blenderkit/paths.py +++ b/blenderkit/paths.py @@ -75,6 +75,9 @@ def get_api_url(): def get_oauth_landing_url(): return get_bkit_url() + BLENDERKIT_OAUTH_LANDING_URL +def get_author_gallery_url(author_id): + return f'{get_bkit_url()}/asset-gallery?query=author_id:{author_id}' + def default_global_dict(): from os.path import expanduser diff --git a/blenderkit/ratings.py b/blenderkit/ratings.py index 48c34a61..800749c8 100644 --- a/blenderkit/ratings.py +++ b/blenderkit/ratings.py @@ -94,7 +94,7 @@ def upload_review_thread(url, reviews, headers): def get_rating(asset_id): - #this function isn't used anywhere,should probably get removed. + # this function isn't used anywhere,should probably get removed. user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key headers = utils.get_headers(api_key) @@ -114,8 +114,13 @@ def update_ratings_quality(self, context): headers = utils.get_headers(api_key) asset = self.id_data - bkit_ratings = asset.bkit_ratings - url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + if asset: + bkit_ratings = asset.bkit_ratings + url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + else: + # this part is for operator rating: + bkit_ratings = self + url = paths.get_api_url() + f'assets/{self.asset_id}/rating/' if bkit_ratings.rating_quality > 0.1: ratings = [('quality', bkit_ratings.rating_quality)] @@ -127,15 +132,19 @@ def update_ratings_work_hours(self, context): api_key = user_preferences.api_key headers = utils.get_headers(api_key) asset = self.id_data - bkit_ratings = asset.bkit_ratings - url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + if asset: + bkit_ratings = asset.bkit_ratings + url = paths.get_api_url() + 'assets/' + asset['asset_data']['id'] + '/rating/' + else: + # this part is for operator rating: + bkit_ratings = self + url = paths.get_api_url() + f'assets/{self.asset_id}/rating/' if bkit_ratings.rating_work_hours > 0.05: ratings = [('working_hours', round(bkit_ratings.rating_work_hours, 1))] tasks_queue.add_task((send_rating_to_thread_work_hours, (url, ratings, headers)), wait=1, only_last=True) - def upload_rating(asset): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key @@ -173,6 +182,7 @@ def upload_rating(asset): if bkit_ratings.rating_quality > 0.1 and bkit_ratings.rating_work_hours > 0.1: s['assets rated'][asset['asset_data']['assetBaseId']] = True + def get_assets_for_rating(): ''' gets assets from scene that could/should be rated by the user. @@ -191,26 +201,6 @@ def get_assets_for_rating(): assets.append(b) return assets -# class StarRatingOperator(bpy.types.Operator): -# """Tooltip""" -# bl_idname = "object.blenderkit_rating" -# bl_label = "Rate the Asset Quality" -# bl_options = {'REGISTER', 'INTERNAL'} -# -# property_name: StringProperty( -# name="Rating Property", -# description="Property that is rated", -# default="", -# ) -# -# rating: IntProperty(name="Rating", description="rating value", default=1, min=1, max=10) -# -# def execute(self, context): -# asset = utils.get_active_asset() -# props = asset.bkit_ratings -# props.rating_quality = self.rating -# return {'FINISHED'} - asset_types = ( ('MODEL', 'Model', 'set of objects'), @@ -254,43 +244,212 @@ class UploadRatingOperator(bpy.types.Operator): return wm.invoke_props_dialog(self) +def stars_enum_callback(self, context): + '''regenerates the enum property used to display rating stars, so that there are filled/empty stars correctly.''' + items = [] + for a in range(0, 10): + if self.rating_quality < a + 1: + icon = 'SOLO_OFF' + else: + icon = 'SOLO_ON' + # has to have something before the number in the value, otherwise fails on registration. + items.append((f'{a + 1}', f'{a + 1}', '', icon, a + 1)) + return items + -def draw_rating(layout, props, prop_name, name): - # layout.label(name) - - row = layout.row(align=True) - # test method - 10 booleans. - # propsx = bpy.context.active_object.bkit_ratings - # for a in range(0, 10): - # pn = f'rq{str(a+1).zfill(2)}' - # if eval('propsx.' + pn) == False: - # icon = 'SOLO_OFF' - # else: - # icon = 'SOLO_ON' - # row.prop(propsx, pn, icon=icon, icon_only=True) - # print(dir(props)) - # new best method - enum with an items callback. ('animates' the stars as item icons) - row.prop(props, 'rating_quality_ui', expand=True, icon_only=True, emboss = False) - # original (operator) method: - # row = layout.row(align=True) - # for a in range(0, 10): - # if eval('props.' + prop_name) < a + 1: - # icon = 'SOLO_OFF' - # else: - # icon = 'SOLO_ON' - # - # op = row.operator('object.blenderkit_rating', icon=icon, emboss=False, text='') - # op.property_name = prop_name - # op.rating = a + 1 +def update_quality_ui(self, context): + '''Converts the _ui the enum into actual quality number.''' + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + if user_preferences.api_key == '': + # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') + # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') + # return + bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', + message='Please login/signup to rate assets. Clicking OK takes you to web login.') + self.rating_quality_ui = '0' + self.rating_quality = int(self.rating_quality_ui) +def update_ratings_work_hours_ui(self, context): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + if user_preferences.api_key == '': + # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') + # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') + # return + bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', + message='Please login/signup to rate assets. Clicking OK takes you to web login.') + self.rating_work_hours_ui = '0' + self.rating_work_hours = float(self.rating_work_hours_ui) + +def update_ratings_work_hours_ui_1_5(self, context): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + if user_preferences.api_key == '': + # ui_panels.draw_not_logged_in(self, message='Please login/signup to rate assets.') + # bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_login_menu') + # return + bpy.ops.wm.blenderkit_login('INVOKE_DEFAULT', + message='Please login/signup to rate assets. Clicking OK takes you to web login.') + self.update_ratings_work_hours_ui_1_5 = '0' + self.rating_work_hours = float(self.update_ratings_work_hours_ui_1_5) + + + +class FastRateMenu(Operator): + """Fast rating of the assets directly in the asset bar - without need to download assets.""" + bl_idname = "wm.blenderkit_menu_rating_upload" + bl_label = "Send Rating" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + message: StringProperty( + name="message", + description="message", + default="Rating asset") + + asset_id: StringProperty( + name="Asset Base Id", + description="Unique name of the asset (hidden)", + default="") + + asset_type: StringProperty( + name="Asset type", + description="asset type", + default="") + + rating_quality: IntProperty(name="Quality", + description="quality of the material", + default=0, + min=-1, max=10, + update=update_ratings_quality) + + # the following enum is only to ease interaction - enums support 'drag over' and enable to draw the stars easily. + rating_quality_ui: EnumProperty(name='rating_quality_ui', + items=stars_enum_callback, + description='Rating stars 0 - 10', + default=None, + update=update_quality_ui, + ) + + rating_work_hours: FloatProperty(name="Work Hours", + description="How many hours did this work take?", + default=0.00, + min=0.0, max=1000, update=update_ratings_work_hours + ) + + rating_work_hours_ui: EnumProperty(name="Work Hours", + description="How many hours did this work take?", + items=[('0', '0', ''), + ('.5', '0.5', ''), + ('1', '1', ''), + ('2', '2', ''), + ('3', '3', ''), + ('4', '4', ''), + ('5', '5', ''), + ('10', '10', ''), + ('15', '15', ''), + ('20', '20', ''), + ('50', '50', ''), + ('100', '100', ''), + ], + default='0', update=update_ratings_work_hours_ui + ) + + rating_work_hours_ui_1_5: EnumProperty(name="Work Hours", + description="How many hours did this work take?", + items=[('0', '0', ''), + ('.2', '0.2', ''), + ('.5', '0.5', ''), + ('1', '1', ''), + ('2', '2', ''), + ('3', '3', ''), + ('4', '4', ''), + ('5', '5', '') + ], + default='0', update=update_ratings_work_hours_ui_1_5 + ) + + @classmethod + def poll(cls, context): + scene = bpy.context.scene + ui_props = scene.blenderkitUI + return ui_props.active_index > -1 + + def draw(self, context): + layout = self.layout + col = layout.column() + + # layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0) + col.label(text=self.message) + row = col.row() + row.prop(self, 'rating_quality_ui', expand=True, icon_only=True, emboss=False) + col.separator() + col.prop(self, 'rating_work_hours') + row = col.row() + if self.asset_type == 'model': + row.prop(self, 'rating_work_hours_ui', expand=True, icon_only=False, emboss=True) + else: + row.prop(self, 'rating_work_hours_ui_1_5', expand=True, icon_only=False, emboss=True) + + def execute(self, context): + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + api_key = user_preferences.api_key + headers = utils.get_headers(api_key) + + url = paths.get_api_url() + f'assets/{self.asset_id}/rating/' + + rtgs = [ + + ] + + self.rating_quality = int(self.rating_quality_ui) + + if self.rating_quality > 0.1: + rtgs.append(('quality', self.rating_quality)) + if self.rating_work_hours > 0.1: + rtgs.append(('working_hours', round(self.rating_work_hours, 1))) + + thread = threading.Thread(target=upload_rating_thread, args=(url, rtgs, headers)) + thread.start() + return {'FINISHED'} + + def invoke(self, context, event): + scene = bpy.context.scene + ui_props = scene.blenderkitUI + if ui_props.active_index > -1: + sr = bpy.context.scene['search results'] + asset_data = dict(sr[ui_props.active_index]) + self.asset_id = asset_data['id'] + self.asset_type = asset_data['assetType'] + self.message = f"Rate asset {asset_data['name']}" + wm = context.window_manager + return wm.invoke_props_dialog(self) + + +def rating_menu_draw(self, context): + layout = self.layout + + ui_props = context.scene.blenderkitUI + sr = bpy.context.scene['search results orig'] + + asset_search_index = ui_props.active_index + if asset_search_index > -1: + asset_data = dict(sr['results'][asset_search_index]) + + col = layout.column() + layout.label(text='Admin rating Tools:') + col.operator_context = 'INVOKE_DEFAULT' + + op = col.operator('wm.blenderkit_menu_rating_upload', text='Fast rate') + op.asset_id = asset_data['id'] + op.asset_type = asset_data['assetType'] + def register_ratings(): - pass; - # bpy.utils.register_class(StarRatingOperator) bpy.utils.register_class(UploadRatingOperator) + bpy.utils.register_class(FastRateMenu) + # bpy.types.OBJECT_MT_blenderkit_asset_menu.append(rating_menu_draw) def unregister_ratings(): pass; # bpy.utils.unregister_class(StarRatingOperator) bpy.utils.unregister_class(UploadRatingOperator) + bpy.utils.unregister_class(FastRateMenu) diff --git a/blenderkit/search.py b/blenderkit/search.py index cee0b14b..6c8d16e2 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -280,7 +280,7 @@ def timer_update(): # TODO here it should check if there are some results, and only open assetbar if this is the case, not search. # if bpy.context.scene.get('search results') is None: search() - preferences.first_run = False + # preferences.first_run = False if preferences.tips_on_start: utils.get_largest_area() ui.update_ui_size(ui.active_area, ui.active_region) @@ -1161,14 +1161,6 @@ def search(category='', get_next=False, author_id=''): scene = bpy.context.scene ui_props = scene.blenderkitUI - ### updating of search categories was moved here, due to the reason that BlenderKit created the blenderkit_data - # folder upon registration of BlenderKit, which wasn't a favourite option for some users (devs running tests). - # user_preferences = bpy.context.preferences.addons['blenderkit'].preferences - # if not user_preferences.first_run: - # api_key = user_preferences.api_key - # if bpy.context.window_manager.get('bkit_categories') is None: - # categories.fetch_categories_thread(api_key) - if ui_props.asset_type == 'MODEL': if not hasattr(scene, 'blenderkit'): return; diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 30195168..7935363d 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -876,7 +876,8 @@ def draw_callback_2d_search(self, context): else: iname = utils.previmg_name(ui_props.active_index) img = bpy.data.images.get(iname) - img.colorspace_settings.name = 'sRGB' + if img: + img.colorspace_settings.name = 'sRGB' gimg = None atip = '' @@ -931,9 +932,21 @@ def mouse_raycast(context, mx, my): # rote = mathutils.Euler((0, 0, math.pi)) randoffset = math.pi if has_hit: - snapped_rotation = snapped_normal.to_track_quat('Z', 'Y').to_euler() - up = Vector((0, 0, 1)) props = bpy.context.scene.blenderkit_models + up = Vector((0, 0, 1)) + + if props.perpendicular_snap: + if snapped_normal.z > 1 - props.perpendicular_snap_threshold: + snapped_normal = Vector((0, 0, 1)) + elif snapped_normal.z < -1 + props.perpendicular_snap_threshold: + snapped_normal = Vector((0, 0, -1)) + elif abs(snapped_normal.z) < props.perpendicular_snap_threshold: + snapped_normal.z = 0 + snapped_normal.normalize() + + snapped_rotation = snapped_normal.to_track_quat('Z', 'Y').to_euler() + + if props.randomize_rotation and snapped_normal.angle(up) < math.radians(10.0): randoffset = props.offset_rotation_amount + math.pi + ( random.random() - 0.5) * props.randomize_rotation_amount @@ -1668,6 +1681,7 @@ class AssetBarOperator(bpy.types.Operator): utils.p('author:', a) search.search(author_id=a) return {'RUNNING_MODAL'} + if event.type == 'X' and ui_props.active_index > -1: # delete downloaded files for this asset sr = bpy.context.scene['search results'] @@ -1845,13 +1859,15 @@ def register_ui(): if not wm.keyconfigs.addon: return km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type='EMPTY') + #asset bar shortcut kmi = km.keymap_items.new(AssetBarOperator.bl_idname, 'SEMI_COLON', 'PRESS', ctrl=False, shift=False) kmi.properties.keep_running = False kmi.properties.do_search = False addon_keymapitems.append(kmi) - # auto open after searching: - kmi = km.keymap_items.new(RunAssetBarWithContext.bl_idname, 'SEMI_COLON', 'PRESS', \ - ctrl=True, shift=True, alt=True) + #fast rating shortcut + wm = bpy.context.window_manager + km = wm.keyconfigs.addon.keymaps['Window'] + kmi = km.keymap_items.new(ratings.FastRateMenu.bl_idname, 'F', 'PRESS', ctrl=False, shift=False) addon_keymapitems.append(kmi) diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index d067afa0..89646862 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -47,7 +47,6 @@ import os import random - # this was moved to separate interface: def draw_ratings(layout, context, asset): @@ -63,9 +62,8 @@ def draw_ratings(layout, context, asset): # layout.template_icon_view(bkit_ratings, property, show_labels=False, scale=6.0, scale_popup=5.0) row = col.row() - row.prop(bkit_ratings , 'rating_quality_ui', expand=True, icon_only=True, emboss=False) - #ratings.draw_rating(col, bkit_ratings, 'rating_quality', 'Quality') - if bkit_ratings.rating_quality>0: + row.prop(bkit_ratings, 'rating_quality_ui', expand=True, icon_only=True, emboss=False) + if bkit_ratings.rating_quality > 0: col.separator() col.prop(bkit_ratings, 'rating_work_hours') # w = context.region.width @@ -81,7 +79,7 @@ def draw_ratings(layout, context, asset): # re-enable layout if included in longer panel -def draw_not_logged_in(source, message = 'Please Login/Signup to use this feature' ): +def draw_not_logged_in(source, message='Please Login/Signup to use this feature'): title = "You aren't logged in" def draw_message(source, context): @@ -104,7 +102,7 @@ def draw_upload_common(layout, props, asset_type, context): row = layout.row(align=True) if props.upload_state != '': - utils.label_multiline(layout, text=props.upload_state, width=context.region.width) + utils.label_multiline(layout, text=props.upload_state, width=context.region.width) if props.uploading: op = layout.operator('object.kill_bg_process', text="", icon='CANCEL') op.process_source = asset_type @@ -193,7 +191,7 @@ def draw_panel_model_upload(self, context): op.process_source = 'MODEL' op.process_type = 'THUMBNAILER' elif props.thumbnail_generating_state != '': - utils.label_multiline(layout, text=props.thumbnail_generating_state) + utils.label_multiline(layout, text=props.thumbnail_generating_state) layout.prop(props, 'description') layout.prop(props, 'tags') @@ -359,7 +357,8 @@ class VIEW3D_PT_blenderkit_model_properties(Panel): o = utils.get_active_model() # o = bpy.context.active_object if o.get('asset_data') is None: - utils.label_multiline(layout, text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') + utils.label_multiline(layout, + text='To upload this asset to BlenderKit, go to the Find and Upload Assets panel.') layout.prop(o, 'name') if o.get('asset_data') is not None: @@ -383,12 +382,14 @@ class VIEW3D_PT_blenderkit_model_properties(Panel): # fun override project, not finished # layout.operator('object.blenderkit_color_corrector') -def draw_rating_asset(self,context,asset): + +def draw_rating_asset(self, context, asset): layout = self.layout col = layout.box() # split = layout.split(factor=0.5) # col1 = split.column() # col2 = split.column() + # print('%s_search' % asset['asset_data']['assetType']) directory = paths.get_temp_dir('%s_search' % asset['asset_data']['assetType']) tpath = os.path.join(directory, asset['asset_data']['thumbnail_small']) for image in bpy.data.images: @@ -401,8 +402,6 @@ def draw_rating_asset(self,context,asset): draw_ratings(col, context, asset=asset) - - class VIEW3D_PT_blenderkit_ratings(Panel): bl_category = "BlenderKit" bl_idname = "VIEW3D_PT_blenderkit_ratings" @@ -418,16 +417,17 @@ class VIEW3D_PT_blenderkit_ratings(Panel): return p def draw(self, context): - #TODO make a list of assets inside asset appending code, to happen only when assets are added to the scene. + # TODO make a list of assets inside asset appending code, to happen only when assets are added to the scene. # draw asset properties here layout = self.layout assets = ratings.get_assets_for_rating() - if len(assets)>0: - layout.label(text = 'Help BlenderKit community') - layout.label(text = 'by rating these assets:') + if len(assets) > 0: + layout.label(text='Help BlenderKit community') + layout.label(text='by rating these assets:') for a in assets: - draw_rating_asset(self, context, asset = a) + draw_rating_asset(self, context, asset=a) + def draw_login_progress(layout): layout.label(text='Login through browser') @@ -520,7 +520,7 @@ def draw_panel_model_rating(self, context): # o = bpy.context.active_object o = utils.get_active_model() # print('ratings active',o) - draw_ratings(self.layout, context, asset = o) # , props) + draw_ratings(self.layout, context, asset=o) # , props) # op.asset_type = 'MODEL' @@ -564,7 +564,7 @@ def draw_panel_material_upload(self, context): op.process_source = 'MATERIAL' op.process_type = 'THUMBNAILER' elif props.thumbnail_generating_state != '': - utils.label_multiline(layout, text=props.thumbnail_generating_state) + utils.label_multiline(layout, text=props.thumbnail_generating_state) if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE'): layout.operator("object.blenderkit_material_thumbnail", text='Render thumbnail with Cycles', icon='EXPORT') @@ -634,12 +634,12 @@ def draw_panel_brush_search(self, context): def draw_panel_brush_ratings(self, context): # props = utils.get_brush_props(context) brush = utils.get_active_brush() - draw_ratings(self.layout, context, asset = brush) # , props) + draw_ratings(self.layout, context, asset=brush) # , props) # # op.asset_type = 'BRUSH' -def draw_login_buttons(layout, invoke = False): +def draw_login_buttons(layout, invoke=False): user_preferences = bpy.context.preferences.addons['blenderkit'].preferences if user_preferences.login_attempt: @@ -782,7 +782,7 @@ class VIEW3D_PT_blenderkit_categories(Panel): s = context.scene ui_props = s.blenderkitUI mode = True - if ui_props.asset_type == 'BRUSH' and not (context.sculpt_object or context.image_paint_object): + if ui_props.asset_type == 'BRUSH' and not (context.sculpt_object or context.image_paint_object): mode = False return ui_props.down_up == 'SEARCH' and mode @@ -820,6 +820,10 @@ class VIEW3D_PT_blenderkit_import_settings(Panel): layout.prop(props, 'randomize_rotation') if props.randomize_rotation: layout.prop(props, 'randomize_rotation_amount') + layout.prop(props, 'perpendicular_snap') + if props.perpendicular_snap: + layout.prop(props,'perpendicular_snap_threshold') + if ui_props.asset_type == 'MATERIAL': props = s.blenderkit_mat layout.prop(props, 'automap') @@ -924,7 +928,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): return; if ui_props.asset_type == 'MODEL': - #utils.label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') + # utils.label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None: draw_panel_model_upload(self, context) else: @@ -933,12 +937,12 @@ class VIEW3D_PT_blenderkit_unified(Panel): draw_panel_scene_upload(self, context) elif ui_props.asset_type == 'MATERIAL': - #utils.label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') + # utils.label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: draw_panel_material_upload(self, context) else: - utils.label_multiline(layout, text='select object with material to upload materials', width=w) + utils.label_multiline(layout, text='select object with material to upload materials', width=w) elif ui_props.asset_type == 'BRUSH': if context.sculpt_object or context.image_paint_object: @@ -972,6 +976,7 @@ class VIEW3D_PT_blenderkit_unified(Panel): if ui_props.asset_type == 'TEXTURE': layout.label(text='not yet implemented') + class BlenderKitWelcomeOperator(bpy.types.Operator): """Login online on BlenderKit webpage""" @@ -993,27 +998,36 @@ class BlenderKitWelcomeOperator(bpy.types.Operator): def draw(self, context): layout = self.layout if self.step == 0: - message = "BlenderKit is an addon that connects to the internet to search and upload for models, materials, and brushes. \n\n Let's start by searching for some cool materials?" + user_preferences = bpy.context.preferences.addons['blenderkit'].preferences + + message = "BlenderKit connects from Blender to an online, " \ + "community built shared library of models, " \ + "materials, and brushes. " \ + "Use addon preferences to set up where files will be saved in the Global directory setting." + + utils.label_multiline(layout, text=message, width=300) + utils.label_multiline(layout, text="\n Let's start by searching for some cool materials?", width=300) else: - message = "This shouldn't be here at all" - utils.label_multiline(layout, text= message, width = 300) + message = "Operator Tutorial called with invalid step" def execute(self, context): if self.step == 0: - #move mouse: - #bpy.context.window_manager.windows[0].cursor_warp(1000, 1000) - #show n-key sidebar (spaces[index] has to be found for view3d too: + # move mouse: + # bpy.context.window_manager.windows[0].cursor_warp(1000, 1000) + # show n-key sidebar (spaces[index] has to be found for view3d too: # bpy.context.window_manager.windows[0].screen.areas[5].spaces[0].show_region_ui = False print('running search no') ui_props = bpy.context.scene.blenderkitUI ui_props.asset_type = 'MATERIAL' - search.search() + bpy.context.scene.blenderkit_mat.search_keywords = 'ice' + # search.search() return {'FINISHED'} def invoke(self, context, event): wm = bpy.context.window_manager return wm.invoke_props_dialog(self) + def draw_asset_context_menu(self, context, asset_data): layout = self.layout ui_props = context.scene.blenderkitUI @@ -1024,10 +1038,11 @@ def draw_asset_context_menu(self, context, asset_data): a = bpy.context.window_manager['bkit authors'].get(author_id) if a is not None: # utils.p('author:', a) + op = layout.operator('wm.url_open', text="Open Author's Website") if a.get('aboutMeUrl') is not None: - op = layout.operator('wm.url_open', text="Open Author's Website") op.url = a['aboutMeUrl'] - + else: + op.url = paths.get_author_gallery_url(a['id']) op = layout.operator('view3d.blenderkit_search', text="Show Assets By Author") op.keywords = '' op.author_id = author_id @@ -1080,6 +1095,8 @@ def draw_asset_context_menu(self, context, asset_data): op.asset_id = asset_data['id'] op.state = 'rejected' + + if author_id == str(profile['user']['id']): layout.label(text='Management tools:') row = layout.row() @@ -1087,9 +1104,13 @@ def draw_asset_context_menu(self, context, asset_data): op = row.operator('object.blenderkit_change_status', text='Delete') op.asset_id = asset_data['id'] op.state = 'deleted' - # else: - # #not an author - can rate - # draw_ratings(layout, context) + + if utils.profile_is_validator(): + layout.label(text='Admin rating Tools:') + + op = layout.operator('wm.blenderkit_menu_rating_upload', text='Fast rate') + op.asset_id = asset_data['id'] + op.asset_type = asset_data['assetType'] class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): @@ -1104,6 +1125,7 @@ class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu): asset_data = sr[ui_props.active_index] draw_asset_context_menu(self, context, asset_data) + class OBJECT_MT_blenderkit_login_menu(bpy.types.Menu): bl_label = "BlenderKit login/signup:" bl_idname = "OBJECT_MT_blenderkit_login_menu" @@ -1188,6 +1210,7 @@ class UrlPopupDialog(bpy.types.Operator): return wm.invoke_props_dialog(self) + class LoginPopupDialog(bpy.types.Operator): """Generate Cycles thumbnail for model assets""" bl_idname = "wm.blenderkit_url_dialog" diff --git a/blenderkit/upload.py b/blenderkit/upload.py index 18e43b8a..14fbe6db 100644 --- a/blenderkit/upload.py +++ b/blenderkit/upload.py @@ -767,7 +767,7 @@ class UploadOperator(Operator): layout.label(text="For updates of thumbnail or model use reupload.") if props.is_private == 'PUBLIC': - ui_panels.label_multiline(layout, text='public assets are validated several hours' + utils.label_multiline(layout, text='public assets are validated several hours' ' or days after upload. Remember always to ' 'test download your asset to a clean file' ' to see if it uploaded correctly.' -- cgit v1.2.3 From fb3d2715ce6f3474fc862fbf5454054730bcb516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Tue, 28 Jul 2020 15:31:28 +0200 Subject: BlenderKit: fix data update -older fines could act as broken (cherry picked from commit e1dae55cca702ef4a140a455d88099d666230c8c) --- blenderkit/append_link.py | 8 ++++++++ blenderkit/download.py | 20 +++++++++++++++++--- blenderkit/search.py | 40 +++++++++++++++++++++++++++++----------- blenderkit/ui.py | 5 +++-- blenderkit/ui_panels.py | 7 ++++++- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/blenderkit/append_link.py b/blenderkit/append_link.py index b6bfb791..2301daac 100644 --- a/blenderkit/append_link.py +++ b/blenderkit/append_link.py @@ -88,6 +88,8 @@ def append_scene(file_name, scenename=None, link=False, fake_user=False): def link_collection(file_name, obnames=[], location=(0, 0, 0), link=False, parent = None, **kwargs): '''link an instanced group - model type asset''' sel = utils.selection_get() + print('link collection') + print(kwargs) with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): scols = [] @@ -115,6 +117,12 @@ def link_collection(file_name, obnames=[], location=(0, 0, 0), link=False, paren main_object.instance_collection = col break; + #sometimes, the lib might already be without the actual link. + if not main_object.instance_collection and kwargs['name']: + col = bpy.data.collections.get(kwargs['name']) + if col: + main_object.instance_collection = col + main_object.name = main_object.instance_collection.name # bpy.ops.wm.link(directory=file_name + "/Collection/", filename=kwargs['name'], link=link, instance_collections=True, diff --git a/blenderkit/download.py b/blenderkit/download.py index 7ec425ce..c14fc84d 100644 --- a/blenderkit/download.py +++ b/blenderkit/download.py @@ -343,6 +343,13 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, parent=kwargs.get('parent')) else: + # parent, newobs = append_link.link_collection(file_names[-1], + # location=downloader['location'], + # rotation=downloader['rotation'], + # link=False, + # name=asset_data['name'], + # parent=kwargs.get('parent')) + parent, newobs = append_link.append_objects(file_names[-1], location=downloader['location'], rotation=downloader['rotation'], @@ -364,10 +371,17 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, name=asset_data['name'], parent=kwargs.get('parent')) else: + # parent, newobs = append_link.link_collection(file_names[-1], + # location=kwargs['model_location'], + # rotation=kwargs['model_rotation'], + # link=False, + # name=asset_data['name'], + # parent=kwargs.get('parent')) parent, newobs = append_link.append_objects(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], link=link, + name=asset_data['name'], parent=kwargs.get('parent')) if parent.type == 'EMPTY' and link: bmin = asset_data['bbox_min'] @@ -723,8 +737,8 @@ def try_finished_append(asset_data, **kwargs): # location=None, material_target for f in file_names: try: os.remove(f) - except: - e = sys.exc_info()[0] + except Exception as e: + # e = sys.exc_info()[0] print(e) pass; done = False @@ -934,7 +948,7 @@ class BlenderkitDownloadOperator(bpy.types.Operator): if self.replace: # cleanup first, assign later. obs = utils.get_selected_replace_adepts() - print(obs) + # print(obs) for ob in obs: print('replace attempt ', ob.name) if self.asset_base_id != '': diff --git a/blenderkit/search.py b/blenderkit/search.py index 6c8d16e2..82bb8f58 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -101,22 +101,40 @@ def refresh_token_timer(): return max(3600, user_preferences.api_key_life - 3600) +def update_ad(ad): + if not ad.get('assetBaseId'): + ad['assetBaseId'] = ad['asset_base_id'] # this should stay ONLY for compatibility with older scenes + ad['assetType'] = ad['asset_type'] # this should stay ONLY for compatibility with older scenes + ad['canDownload'] = ad['can_download'] # this should stay ONLY for compatibility with older scenes + ad['verificationStatus'] = ad['verification_status'] # this should stay ONLY for compatibility with older scenes + ad['author'] = {} + ad['author']['id'] = ad['author_id'] # this should stay ONLY for compatibility with older scenes + return ad def update_assets_data(): # updates assets data on scene load. '''updates some properties that were changed on scenes with older assets. The properties were mainly changed from snake_case to CamelCase to fit the data that is coming from the server. ''' - for ob in bpy.context.scene.objects: - if ob.get('asset_data') != None: - ad = ob['asset_data'] - if not ad.get('assetBaseId'): - ad['assetBaseId'] = ad['asset_base_id'], # this should stay ONLY for compatibility with older scenes - ad['assetType'] = ad['asset_type'], # this should stay ONLY for compatibility with older scenes - ad['canDownload'] = ad['can_download'], # this should stay ONLY for compatibility with older scenes - ad['verificationStatus'] = ad[ - 'verification_status'], # this should stay ONLY for compatibility with older scenes - ad['author'] = {} - ad['author']['id'] = ad['author_id'], # this should stay ONLY for compatibility with older scenes + data = bpy.data + + datablocks = [ + bpy.data.objects, + bpy.data.materials, + bpy.data.brushes, + ] + for dtype in datablocks: + for block in dtype: + if block.get('asset_data') != None: + update_ad(block['asset_data']) + + dicts = [ + 'assets used', + 'assets rated', + ] + for d in dicts: + for k in d.keys(): + update_ad(d[k]) + # bpy.context.scene['assets used'][ad] = ad @persistent diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 7935363d..80b7ef2c 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1033,9 +1033,10 @@ def is_rating_possible(): m = ao.active_material if m is not None: ad = m.get('asset_data') - if ad is not None: + if ad is not None and ad.get('assetBaseId'): rated = bpy.context.scene['assets rated'].get(ad['assetBaseId']) - return True, rated, m, ad + if rated: + return True, rated, m, ad # if t>2 and t<2.5: # ui_props.rating_on = False diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py index 89646862..a591e76e 100644 --- a/blenderkit/ui_panels.py +++ b/blenderkit/ui_panels.py @@ -1048,7 +1048,12 @@ def draw_asset_context_menu(self, context, asset_data): op.author_id = author_id op = layout.operator('view3d.blenderkit_search', text='Search Similar') - op.keywords = asset_data['name'] + ' ' + asset_data['description'] + ' ' + ' '.join(asset_data['tags']) + #build search string from description and tags: + op.keywords = asset_data['name'] + if asset_data.get('description'): + op.keywords += ' ' + asset_data.get('description') + op.keywords += ' '.join(asset_data.get('tags')) + if asset_data.get('canDownload') != 0: if len(bpy.context.selected_objects) > 0 and ui_props.asset_type == 'MODEL': aob = bpy.context.active_object -- cgit v1.2.3 From 5cad98dce57ca6d9136d2cdfc8599250ad2cb4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Wed, 29 Jul 2020 20:11:17 +0200 Subject: BlenderKit: fix appending -this was broken due to API changes. Also no need for so much magic now since the default append just works well. -fix a bug in previous commit (asset update) (cherry picked from commit 3bdb5f41aea4897762edd09a71b0bfa8b0b7bc10) --- blenderkit/append_link.py | 39 ++++++++++++++++++++++++++++++++++++--- blenderkit/download.py | 32 +++++++++++--------------------- blenderkit/search.py | 13 +++++++++---- blenderkit/ui.py | 2 +- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/blenderkit/append_link.py b/blenderkit/append_link.py index 2301daac..5af63fe1 100644 --- a/blenderkit/append_link.py +++ b/blenderkit/append_link.py @@ -189,12 +189,44 @@ def append_particle_system(file_name, obnames=[], location=(0, 0, 0), link=False def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs): '''append objects into scene individually''' + #simplified version of append + scene = bpy.context.scene + sel = utils.selection_get() + bpy.ops.object.select_all(action='DESELECT') + + path = file_name + "\\Collection\\" + object_name = kwargs.get('name') + fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D') + bpy.ops.wm.append(fc,filename=object_name, directory=path) + + + return_obs = [] + for ob in bpy.context.scene.objects: + if ob.select_get(): + return_obs.append(ob) + if not ob.parent: + main_object = ob + ob.location = location + + if kwargs.get('rotation'): + main_object.rotation_euler = kwargs['rotation'] + + if kwargs.get('parent') is not None: + main_object.parent = bpy.data.objects[kwargs['parent']] + main_object.matrix_world.translation = location + + bpy.ops.object.select_all(action='DESELECT') + utils.selection_set(sel) + + return main_object, return_obs with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): sobs = [] - for ob in data_from.objects: - if ob in obnames or obnames == []: - sobs.append(ob) + for col in data_from.collections: + if col == kwargs.get('name'): + for ob in col.objects: + if ob in obnames or obnames == []: + sobs.append(ob) data_to.objects = sobs # data_to.objects = data_from.objects#[name for name in data_from.objects if name.startswith("house")] @@ -221,6 +253,7 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar hidden_objects.append(obj) obj.hide_viewport = False return_obs.append(obj) + # Only after all objects are in scene! Otherwise gets broken relationships if link == True: bpy.ops.object.make_local(type='SELECT_OBJECT') diff --git a/blenderkit/download.py b/blenderkit/download.py index c14fc84d..a93611c0 100644 --- a/blenderkit/download.py +++ b/blenderkit/download.py @@ -307,17 +307,15 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, # copy for override al = sprops.append_link - import_as = sprops.import_as # set consistency for objects already in scene, otherwise this literally breaks blender :) ain = asset_in_scene(asset_data) + # override based on history if ain is not False: if ain == 'LINKED': al = 'LINK' - import_as = 'GROUP' else: al = 'APPEND' - import_as = 'INDIVIDUAL' # first get conditions for append link link = al == 'LINK' @@ -334,7 +332,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, name=asset_data['name']) return - if sprops.import_as == 'GROUP': + if link: parent, newobs = append_link.link_collection(file_names[-1], location=downloader['location'], rotation=downloader['rotation'], @@ -343,12 +341,6 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, parent=kwargs.get('parent')) else: - # parent, newobs = append_link.link_collection(file_names[-1], - # location=downloader['location'], - # rotation=downloader['rotation'], - # link=False, - # name=asset_data['name'], - # parent=kwargs.get('parent')) parent, newobs = append_link.append_objects(file_names[-1], location=downloader['location'], @@ -363,7 +355,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, parent.empty_display_size = size_min elif kwargs.get('model_location') is not None: - if sprops.import_as == 'GROUP': + if link: parent, newobs = append_link.link_collection(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], @@ -371,18 +363,14 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, name=asset_data['name'], parent=kwargs.get('parent')) else: - # parent, newobs = append_link.link_collection(file_names[-1], - # location=kwargs['model_location'], - # rotation=kwargs['model_rotation'], - # link=False, - # name=asset_data['name'], - # parent=kwargs.get('parent')) parent, newobs = append_link.append_objects(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], link=link, name=asset_data['name'], parent=kwargs.get('parent')) + + #scale Empty for assets, so they don't clutter the scene. if parent.type == 'EMPTY' and link: bmin = asset_data['bbox_min'] bmax = asset_data['bbox_max'] @@ -758,10 +746,12 @@ def asset_in_scene(asset_data): asset_data['file_name'] = ad['file_name'] asset_data['url'] = ad['url'] - c = bpy.data.collections.get(ad['name']) - if c is not None: - if c.users > 0: - return 'LINKED' + #browse all collections since linked collections can have same name. + for c in bpy.data.collections: + if c.name == ad['name']: + #there can also be more linked collections with same name, we need to check id. + if c.library and c.library.get('asset_data') and c.library['asset_data']['assetBaseId'] == id: + return 'LINKED' return 'APPENDED' return False diff --git a/blenderkit/search.py b/blenderkit/search.py index 82bb8f58..25d85d88 100644 --- a/blenderkit/search.py +++ b/blenderkit/search.py @@ -131,10 +131,15 @@ def update_assets_data(): # updates assets data on scene load. 'assets used', 'assets rated', ] - for d in dicts: - for k in d.keys(): - update_ad(d[k]) - # bpy.context.scene['assets used'][ad] = ad + for s in bpy.data.scenes: + for k in dicts: + d = s.get(k) + if not d: + continue; + + for k in d.keys(): + update_ad(d[k]) + # bpy.context.scene['assets used'][ad] = ad @persistent diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 80b7ef2c..7ae6988a 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -738,7 +738,7 @@ def draw_callback_2d_search(self, context): highlight = (1, 1, 1, .2) # highlight = (1, 1, 1, 0.8) # background of asset bar - if not ui_props.dragging: + if not ui_props.dragging and ui_props.hcount>0: search_results = s.get('search results') search_results_orig = s.get('search results orig') if search_results == None: -- cgit v1.2.3 From 2f73d67f25ccc538717b10d6bfbcafbc3591c372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Thu, 30 Jul 2020 11:27:57 +0200 Subject: BlenderKit: fix several issues caused by context.copy() now creating simple context everywhere instead of the context.copy() which actually: - could not work if other addons were creating any custom subclasses on context - managed to crash blender in my tests. --- blenderkit/ui.py | 7 +------ blenderkit/utils.py | 51 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 7ae6988a..2822b826 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1796,12 +1796,7 @@ class UndoWithContext(bpy.types.Operator): message: StringProperty('Undo Message', default='BlenderKit operation') def execute(self, context): - C_dict = bpy.context.copy() - C_dict.update(region='WINDOW') - if context.area is None or context.area.type != 'VIEW_3D': - w, a, r = utils.get_largest_area() - override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} - C_dict.update(override) + C_dict = utils.get_fake_context(context) bpy.ops.ed.undo_push(C_dict, 'INVOKE_REGION_WIN', message=self.message) return {'FINISHED'} diff --git a/blenderkit/utils.py b/blenderkit/utils.py index effc2627..5236aabb 100644 --- a/blenderkit/utils.py +++ b/blenderkit/utils.py @@ -32,9 +32,6 @@ import json import os import sys - - - ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000 BELOW_NORMAL_PRIORITY_CLASS = 0x00004000 HIGH_PRIORITY_CLASS = 0x00000080 @@ -42,12 +39,14 @@ IDLE_PRIORITY_CLASS = 0x00000040 NORMAL_PRIORITY_CLASS = 0x00000020 REALTIME_PRIORITY_CLASS = 0x00000100 + def get_process_flags(): flags = BELOW_NORMAL_PRIORITY_CLASS if sys.platform != 'win32': # TODO test this on windows flags = 0 return flags + def activate(ob): bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) @@ -97,11 +96,12 @@ def get_selected_models(): parents.append(ob) done[ob] = True - #if no blenderkit - like objects were found, use the original selection. + # if no blenderkit - like objects were found, use the original selection. if len(parents) == 0: parents = obs return parents + def get_selected_replace_adepts(): ''' Detect all hierarchies that contain either asset data from selection, or selected objects themselves. @@ -127,11 +127,12 @@ def get_selected_replace_adepts(): done[ob] = True # print(parents) - #if no blenderkit - like objects were found, use the original selection. + # if no blenderkit - like objects were found, use the original selection. if len(parents) == 0: parents = obs return parents + def get_search_props(): scene = bpy.context.scene if scene is None: @@ -238,7 +239,7 @@ def load_prefs(): def save_prefs(self, context): # first check context, so we don't do this on registration or blender startup - if not bpy.app.background: #(hasattr kills blender) + if not bpy.app.background: # (hasattr kills blender) user_preferences = bpy.context.preferences.addons['blenderkit'].preferences # we test the api key for length, so not a random accidentally typed sequence gets saved. lk = len(user_preferences.api_key) @@ -263,16 +264,18 @@ def save_prefs(self, context): except Exception as e: print(e) -def get_hidden_texture(tpath, bdata_name, force_reload = False): - i = get_hidden_image(tpath, bdata_name, force_reload = force_reload) + +def get_hidden_texture(tpath, bdata_name, force_reload=False): + i = get_hidden_image(tpath, bdata_name, force_reload=force_reload) bdata_name = f".{bdata_name}" t = bpy.data.textures.get(bdata_name) if t is None: t = bpy.data.textures.new('.test', 'IMAGE') - if t.image!= i: + if t.image != i: t.image = i return t + def get_hidden_image(tpath, bdata_name, force_reload=False): hidden_name = '.%s' % bdata_name img = bpy.data.images.get(hidden_name) @@ -348,16 +351,19 @@ def get_hierarchy(ob): obs.append(o) return obs -def select_hierarchy(ob, state = True): + +def select_hierarchy(ob, state=True): obs = get_hierarchy(ob) for ob in obs: ob.select_set(state) return obs + def delete_hierarchy(ob): obs = get_hierarchy(ob) bpy.ops.object.delete({"selected_objects": obs}) + def get_bounds_snappable(obs, use_modifiers=False): # progress('getting bounds of object(s)') parent = obs[0] @@ -473,13 +479,15 @@ def get_headers(api_key): headers["Authorization"] = "Bearer %s" % api_key return headers + def scale_2d(v, s, p): '''scale a 2d vector with a pivot''' return (p[0] + s[0] * (v[0] - p[0]), p[1] + s[1] * (v[1] - p[1])) -def scale_uvs(ob, scale = 1.0, pivot = Vector((.5,.5))): + +def scale_uvs(ob, scale=1.0, pivot=Vector((.5, .5))): mesh = ob.data - if len(mesh.uv_layers)>0: + if len(mesh.uv_layers) > 0: uv = mesh.uv_layers[mesh.uv_layers.active_index] # Scale a UV map iterating over its coordinates to a given scale and with a pivot point @@ -488,7 +496,7 @@ def scale_uvs(ob, scale = 1.0, pivot = Vector((.5,.5))): # map uv cubic and switch of auto tex space and set it to 1,1,1 -def automap(target_object=None, target_slot=None, tex_size=1, bg_exception=False, just_scale = False): +def automap(target_object=None, target_slot=None, tex_size=1, bg_exception=False, just_scale=False): from blenderkit import bg_blender as bg s = bpy.context.scene mat_props = s.blenderkit_mat @@ -540,9 +548,10 @@ def automap(target_object=None, target_slot=None, tex_size=1, bg_exception=False # by now, it takes the basic uv map = 1 meter. also, it now doeasn't respect more materials on one object, # it just scales whole UV. if just_scale: - scale_uvs(tob, scale=Vector((1/tex_size, 1/tex_size))) + scale_uvs(tob, scale=Vector((1 / tex_size, 1 / tex_size))) bpy.context.view_layer.objects.active = actob + def name_update(): props = get_upload_props() if props.name_old != props.name: @@ -562,12 +571,14 @@ def name_update(): asset = get_active_asset() asset.name = fname + def params_to_dict(params): params_dict = {} for p in params: params_dict[p['parameterType']] = p['value'] return params_dict + def dict_to_params(inputs, parameters=None): if parameters == None: parameters = [] @@ -605,6 +616,7 @@ def profile_is_validator(): return True return False + def guard_from_crash(): '''Blender tends to crash when trying to run some functions with the addon going through unregistration process.''' if bpy.context.preferences.addons.get('blenderkit') is None: @@ -614,7 +626,7 @@ def guard_from_crash(): return True -def get_largest_area( area_type = 'VIEW_3D'): +def get_largest_area(area_type='VIEW_3D'): maxsurf = 0 maxa = None maxw = None @@ -637,11 +649,12 @@ def get_largest_area( area_type = 'VIEW_3D'): active_region = region return maxw, maxa, region -def get_fake_context(context, area_type = 'VIEW_3D'): - C_dict = context.copy() + +def get_fake_context(context, area_type='VIEW_3D'): + C_dict = {} # context.copy() #context.copy was a source of problems - incompatibility with addons that also define context C_dict.update(region='WINDOW') if context.area is None or context.area.type != area_type: - w, a, r = get_largest_area(area_type = area_type) + w, a, r = get_largest_area(area_type=area_type) override = {'window': w, 'screen': w.screen, 'area': a, 'region': r} C_dict.update(override) @@ -675,4 +688,4 @@ def label_multiline(layout, text='', icon='NONE', width=-1): if li > maxlines: break; layout.label(text=l, icon=icon) - icon = 'NONE' \ No newline at end of file + icon = 'NONE' -- cgit v1.2.3 From 666379fe7cf7455b892d5e1b66f3928102cca3a1 Mon Sep 17 00:00:00 2001 From: Antonio Vazquez Date: Fri, 31 Jul 2020 10:02:07 +0200 Subject: Measureit: Fix unreported missing scene scale in arcs The scale of the scene was not applied to the arcs. This fix is related to D8418, but with minor changes. --- measureit/measureit_geometry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/measureit/measureit_geometry.py b/measureit/measureit_geometry.py index 1f314f7f..998bc038 100644 --- a/measureit/measureit_geometry.py +++ b/measureit/measureit_geometry.py @@ -413,7 +413,7 @@ def draw_segments(context, myobj, op, region, rv3d): if ms.gltype == 11: # arc # print length or arc and angle if ms.glarc_len is True: - tx_dist = ms.glarc_txlen + format_distance(fmt, units, arc_length) + tx_dist = ms.glarc_txlen + format_distance(fmt, units, arc_length * scale) else: tx_dist = " " @@ -453,7 +453,7 @@ def draw_segments(context, myobj, op, region, rv3d): if scene.measureit_gl_show_d is True and ms.gldist is True and \ ms.glarc_rad is True: tx_dist = ms.glarc_txradio + format_distance(fmt, units, - dist * scene.measureit_scale_factor) + dist * scene.measureit_scale_factor * scale) else: tx_dist = " " if ms.gltype == 2: -- cgit v1.2.3 From 7da2a313f91e3ec0c3ae3602c1e09e09c8c19f21 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Sat, 1 Aug 2020 17:22:10 +1000 Subject: Fix T79438: Export UV Layout doesn't respect UVs alpha --- io_mesh_uv_layout/export_uv_png.py | 1 + 1 file changed, 1 insertion(+) diff --git a/io_mesh_uv_layout/export_uv_png.py b/io_mesh_uv_layout/export_uv_png.py index f831402f..7fd3ba09 100644 --- a/io_mesh_uv_layout/export_uv_png.py +++ b/io_mesh_uv_layout/export_uv_png.py @@ -30,6 +30,7 @@ def export(filepath, face_data, colors, width, height, opacity): offscreen.bind() try: + bgl.glClearColor(0.0, 0.0, 0.0, 0.0) bgl.glClear(bgl.GL_COLOR_BUFFER_BIT) draw_image(face_data, opacity) -- cgit v1.2.3 From e44e5845ee2b9e8de3b06d5678f60eb0ea09bc4f Mon Sep 17 00:00:00 2001 From: Maurice Raybaud Date: Sun, 2 Aug 2020 00:07:39 +0200 Subject: fix: Texture slots for world and materials fix: Orthographic and perspective camera view angle thanks to Iari Marino add: some numpy functions to export mesh possibly faster in next version fix: parametric surfaces much accelerated and now actually usable (max gradient defaults were wrong from the time of their implementation in most pov literature. Thanks to William F. Pokorny for finding this out! add: very basic "blurry reflection" hack for when using plain official POV add: push of (as of yet badly formatted) feedback to interactive console add: POV centric workspace, default when addon is left activated from previous session. add: Sound signal support on finished render (set from addon preferences) add: support for pov 3.8 and decremented in a few areas, waiting for the release add: freestyle interface with convoluted workflow currently but preparing for next release. fix: commented out Charset feature because POV 3.8 auto detects encoding fix: a few dot notation look ups aliased and removed fix: restored some more removed properties from 2.79 ( a few remain to do) fix: texture mapped specular max value increased fix: faster defaults for radiosity fix: many default texture influences switched to 1 because boolean enabling is required anyway so 0 was a bad default fix: some icons were missing since 2.8 fix: some formatting improvement was started --- render_povray/__init__.py | 358 ++++++++++++++++----- render_povray/nodes.py | 33 +- render_povray/primitives.py | 55 ++-- render_povray/render.py | 362 +++++++++++++++------ render_povray/shading.py | 144 +++++++-- render_povray/ui.py | 753 +++++++++++++++++++++++++++++++++++--------- 6 files changed, 1303 insertions(+), 402 deletions(-) diff --git a/render_povray/__init__.py b/render_povray/__init__.py index 7a6332ee..fded5cff 100644 --- a/render_povray/__init__.py +++ b/render_povray/__init__.py @@ -25,16 +25,16 @@ Scene Description Language. The script has been split in as few files as possible : ___init__.py : - Initialize variables + Initialize properties update_files.py - Update new variables to values from older API. This file needs an update. + Update new variables to values from older API. This file needs an update ui.py : - Provide property buttons for the user to set up the variables. + Provide property buttons for the user to set up the variables primitives.py : - Display some POV native primitives in 3D view for input and output. + Display some POV native primitives in 3D view for input and output shading.py Translate shading properties to declared textures at the top of a pov file @@ -50,7 +50,7 @@ render.py : Along these essential files also coexist a few additional libraries to help make -Blender stand up to other POV IDEs such as povwin or QTPOV. +Blender stand up to other POV IDEs such as povwin or QTPOV presets : Material (sss) apple.py ; chicken.py ; cream.py ; Ketchup.py ; marble.py ; @@ -64,11 +64,14 @@ Blender stand up to other POV IDEs such as povwin or QTPOV. 01_Clear_Blue_Sky.py ; 02_Partly_Hazy_Sky.py ; 03_Overcast_Sky.py ; 04_Cartoony_Sky.py ; 05_Under_Water.py ; Light - 01_(5400K)_Direct_Sun.py ; 02_(5400K)_High_Noon_Sun.py ; + 01_(4800K)_Direct_Sun.py ; + 02_(5400K)_High_Noon_Sun.py ; 03_(6000K)_Daylight_Window.py ; 04_(6000K)_2500W_HMI_(Halogen_Metal_Iodide).py ; - 05_(4000K)_100W_Metal_Halide.py ; 06_(3200K)_100W_Quartz_Halogen.py ; - 07_(2850K)_100w_Tungsten.py ; 08_(2600K)_40w_Tungsten.py ; + 05_(4000K)_100W_Metal_Halide.py ; + 06_(3200K)_100W_Quartz_Halogen.py ; + 07_(2850K)_100w_Tungsten.py ; + 08_(2600K)_40w_Tungsten.py ; 09_(5000K)_75W_Full_Spectrum_Fluorescent_T12.py ; 10_(4300K)_40W_Vintage_Fluorescent_T12.py ; 11_(5000K)_18W_Standard_Fluorescent_T8 ; @@ -78,10 +81,13 @@ Blender stand up to other POV IDEs such as povwin or QTPOV. 15_(3200K)_40W_Induction_Fluorescent.py ; 16_(2100K)_150W_High_Pressure_Sodium.py ; 17_(1700K)_135W_Low_Pressure_Sodium.py ; - 18_(6800K)_175W_Mercury_Vapor.py ; 19_(5200K)_700W_Carbon_Arc.py ; - 20_(6500K)_15W_LED_Spot.py ; 21_(2700K)_7W_OLED_Panel.py ; + 18_(6800K)_175W_Mercury_Vapor.py ; + 19_(5200K)_700W_Carbon_Arc.py ; + 20_(6500K)_15W_LED_Spot.py ; + 21_(2700K)_7W_OLED_Panel.py ; 22_(30000K)_40W_Black_Light_Fluorescent.py ; - 23_(30000K)_40W_Black_Light_Bulb.py; 24_(1850K)_Candle.py + 23_(30000K)_40W_Black_Light_Bulb.py; + 24_(1850K)_Candle.py templates: abyss.pov ; biscuit.pov ; bsp_Tango.pov ; chess2.pov ; cornell.pov ; diffract.pov ; diffuse_back.pov ; float5 ; @@ -98,20 +104,23 @@ bl_info = { "Bastien Montagne, " "Constantin Rahn, " "Silvio Falcinelli", - "version": (0, 1, 0), + "version": (0, 1, 1), "blender": (2, 81, 0), "location": "Render Properties > Render Engine > Persistence of Vision", "description": "Persistence of Vision integration for blender", "doc_url": "{BLENDER_MANUAL_URL}/addons/render/povray.html", "category": "Render", + "warning": "Under active development, seeking co-maintainer(s)", } if "bpy" in locals(): import importlib importlib.reload(ui) + importlib.reload(nodes) importlib.reload(render) importlib.reload(shading) + importlib.reload(primitives) importlib.reload(update_files) else: @@ -121,13 +130,18 @@ else: import nodeitems_utils # for Nodes from nodeitems_utils import NodeCategory, NodeItem # for Nodes from bl_operators.presets import AddPresetBase - from bpy.types import AddonPreferences, PropertyGroup + from bpy.types import ( + AddonPreferences, + PropertyGroup, + NodeSocket, + ) + from bpy.props import ( + FloatVectorProperty, StringProperty, BoolProperty, IntProperty, FloatProperty, - FloatVectorProperty, EnumProperty, PointerProperty, CollectionProperty, @@ -137,39 +151,94 @@ else: def string_strip_hyphen(name): - """Remove hyphen characters from a string to avoid POV errors.""" + """Remove hyphen characters from a string to avoid POV errors""" return name.replace("-", "") +def pov_context_tex_datablock(context): + """Texture context type recreated as deprecated in blender 2.8""" + + idblock = context.brush + if idblock and context.scene.texture_context == 'OTHER': + return idblock + + # idblock = bpy.context.active_object.active_material + idblock = context.view_layer.objects.active.active_material + if idblock and context.scene.texture_context == 'MATERIAL': + return idblock + + idblock = context.world + if idblock and context.scene.texture_context == 'WORLD': + return idblock + + idblock = context.light + if idblock and context.scene.texture_context == 'LIGHT': + return idblock + + if context.particle_system and context.scene.texture_context == 'PARTICLES': + idblock = context.particle_system.settings + + return idblock + + idblock = context.line_style + if idblock and context.scene.texture_context == 'LINESTYLE': + return idblock + +def brush_texture_update(self, context): + + """Brush texture rolldown must show active slot texture props""" + idblock = pov_context_tex_datablock(context) + if idblock is not None: + #mat = context.view_layer.objects.active.active_material + idblock = pov_context_tex_datablock(context) + slot = idblock.pov_texture_slots[idblock.pov.active_texture_index] + tex = slot.texture + + if tex: + # Switch paint brush to active texture so slot and settings remain contextual + bpy.context.tool_settings.image_paint.brush.texture = bpy.data.textures[tex] + bpy.context.tool_settings.image_paint.brush.mask_texture = bpy.data.textures[tex] def active_texture_name_from_uilist(self, context): - mat = context.scene.view_layers["View Layer"].objects.active.active_material - index = mat.pov.active_texture_index - name = mat.pov_texture_slots[index].name - newname = mat.pov_texture_slots[index].texture - tex = bpy.data.textures[name] - tex.name = newname - mat.pov_texture_slots[index].name = newname + + idblock = pov_context_tex_datablock(context) + #mat = context.view_layer.objects.active.active_material + if idblock is not None: + index = idblock.pov.active_texture_index + name = idblock.pov_texture_slots[index].name + newname = idblock.pov_texture_slots[index].texture + tex = bpy.data.textures[name] + tex.name = newname + idblock.pov_texture_slots[index].name = newname def active_texture_name_from_search(self, context): - mat = context.scene.view_layers["View Layer"].objects.active.active_material - index = mat.pov.active_texture_index - name = mat.pov_texture_slots[index].texture_search + """Texture rolldown to change the data linked by an existing texture""" + idblock = pov_context_tex_datablock(context) + #mat = context.view_layer.objects.active.active_material + if idblock is not None: + index = idblock.pov.active_texture_index + slot = idblock.pov_texture_slots[index] + name = slot.texture_search + try: tex = bpy.data.textures[name] - mat.pov_texture_slots[index].name = name - mat.pov_texture_slots[index].texture = name + slot.name = name + slot.texture = name + # Switch paint brush to this texture so settings remain contextual + #bpy.context.tool_settings.image_paint.brush.texture = tex + #bpy.context.tool_settings.image_paint.brush.mask_texture = tex except: pass + ############################################################################### # Scene POV properties. ############################################################################### class RenderPovSettingsScene(PropertyGroup): - """Declare scene level properties controllable in UI and translated to POV.""" + """Declare scene level properties controllable in UI and translated to POV""" # Linux SDL-window enable sdl_window_enable: BoolProperty( @@ -770,7 +839,7 @@ class RenderPovSettingsScene(PropertyGroup): name="Error Bound", description="One of the two main speed/quality tuning values, " "lower values are more accurate", - min=0.0, max=1000.0, soft_min=0.1, soft_max=10.0, default=1.8 + min=0.0, max=1000.0, soft_min=0.1, soft_max=10.0, default=10.0 ) radio_gray_threshold: FloatProperty( @@ -837,14 +906,15 @@ class RenderPovSettingsScene(PropertyGroup): name="Pretrace Start", description="Fraction of the screen width which sets the size of the " "blocks in the mosaic preview first pass", - min=0.01, max=1.00, soft_min=0.02, soft_max=1.0, default=0.08 + min=0.005, max=1.00, soft_min=0.02, soft_max=1.0, default=0.04 ) - + # XXX TODO set automatically to pretrace_end = 8 / max (image_width, image_height) + # for non advanced mode radio_pretrace_end: FloatProperty( name="Pretrace End", description="Fraction of the screen width which sets the size of the blocks " "in the mosaic preview last pass", - min=0.000925, max=1.00, soft_min=0.01, soft_max=1.00, default=0.04, precision=3 + min=0.000925, max=1.00, soft_min=0.01, soft_max=1.00, default=0.004, precision=3 ) ############################################################################### @@ -856,19 +926,28 @@ class MaterialTextureSlot(PropertyGroup): bl_idname="pov_texture_slots", bl_description="Texture_slots from Blender-2.79", + # Adding a "real" texture datablock as property is not possible + # (or at least not easy through a dynamically populated EnumProperty). + # That's why we'll use a prop_search() UILayout function in ui.py. + # So we'll assign the name of the needed texture datablock to the below StringProperty. texture : StringProperty(update=active_texture_name_from_uilist) - texture_search : StringProperty(update=active_texture_name_from_search) + # and use another temporary StringProperty to change the linked data + texture_search : StringProperty( + name="", + update = active_texture_name_from_search, + description = "Browse Texture to be linked", + ) alpha_factor: FloatProperty( name="Alpha", description="Amount texture affects alpha", - default = 0.0, + default = 1.0, ) ambient_factor: FloatProperty( name="", description="Amount texture affects ambient", - default = 0.0, + default = 1.0, ) bump_method: EnumProperty( @@ -897,49 +976,49 @@ class MaterialTextureSlot(PropertyGroup): density_factor: FloatProperty( name="", description="Amount texture affects density", - default = 0.0, + default = 1.0, ) diffuse_color_factor: FloatProperty( name="", description="Amount texture affects diffuse color", - default = 0.0, + default = 1.0, ) diffuse_factor: FloatProperty( name="", description="Amount texture affects diffuse reflectivity", - default = 0.0, + default = 1.0, ) displacement_factor: FloatProperty( name="", description="Amount texture displaces the surface", - default = 0.0, + default = 0.2, ) emission_color_factor: FloatProperty( name="", description="Amount texture affects emission color", - default = 0.0, + default = 1.0, ) emission_factor: FloatProperty( name="", description="Amount texture affects emission", - default = 0.0, + default = 1.0, ) emit_factor: FloatProperty( name="", description="Amount texture affects emission", - default = 0.0, + default = 1.0, ) hardness_factor: FloatProperty( name="", description="Amount texture affects hardness", - default = 0.0, + default = 1.0, ) mapping: EnumProperty( @@ -985,13 +1064,13 @@ class MaterialTextureSlot(PropertyGroup): mirror_factor: FloatProperty( name="", description="Amount texture affects mirror color", - default = 0.0, + default = 1.0, ) normal_factor: FloatProperty( name="", description="Amount texture affects normal values", - default = 0.0, + default = 1.0, ) normal_map_space: EnumProperty( @@ -1013,39 +1092,65 @@ class MaterialTextureSlot(PropertyGroup): raymir_factor: FloatProperty( name="", description="Amount texture affects ray mirror", - default = 0.0, + default = 1.0, ) reflection_color_factor: FloatProperty( name="", description="Amount texture affects color of out-scattered light", - default = 0.0, + default = 1.0, ) reflection_factor: FloatProperty( name="", description="Amount texture affects brightness of out-scattered light", - default = 0.0, + default = 1.0, ) scattering_factor: FloatProperty( name="", description="Amount texture affects scattering", - default = 0.0, + default = 1.0, ) specular_color_factor: FloatProperty( name="", description="Amount texture affects specular color", - default = 0.0, + default = 1.0, ) specular_factor: FloatProperty( name="", description="Amount texture affects specular reflectivity", - default = 0.0, + default = 1.0, + ) + + offset: FloatVectorProperty( + name="Offset", + description=("Fine tune of the texture mapping X, Y and Z locations "), + precision=4, + step=0.1, + soft_min= -100.0, + soft_max=100.0, + default=(0.0,0.0,0.0), + options={'ANIMATABLE'}, + subtype='TRANSLATION', ) + scale: FloatVectorProperty( + name="Size", + subtype='XYZ', + size=3, + description="Set scaling for the texture’s X, Y and Z sizes ", + precision=4, + step=0.1, + soft_min= -100.0, + soft_max=100.0, + default=(1.0,1.0,1.0), + options={'ANIMATABLE'}, + ) + + texture_coords: EnumProperty( name="", description="", @@ -1068,13 +1173,13 @@ class MaterialTextureSlot(PropertyGroup): translucency_factor: FloatProperty( name="", description="Amount texture affects translucency", - default = 0.0, + default = 1.0, ) transmission_color_factor: FloatProperty( name="", description="Amount texture affects result color after light has been scattered/absorbed", - default = 0.0, + default = 1.0, ) use: BoolProperty( @@ -1095,6 +1200,12 @@ class MaterialTextureSlot(PropertyGroup): default = False, ) + use_interpolation: BoolProperty( + name="", + description="Interpolates pixels using selected filter ", + default = False, + ) + use_map_alpha: BoolProperty( name="", description="Causes the texture to affect the alpha value", @@ -1110,7 +1221,7 @@ class MaterialTextureSlot(PropertyGroup): use_map_color_diffuse: BoolProperty( name="", description="Causes the texture to affect basic color of the material", - default = False, + default = True, ) use_map_color_emission: BoolProperty( @@ -1234,7 +1345,7 @@ class MaterialTextureSlot(PropertyGroup): ) -#######################################" +####################################### blend_factor: FloatProperty( name="Blend", @@ -1328,10 +1439,10 @@ bpy.types.ID.texture_context = EnumProperty( default = 'MATERIAL', ) -bpy.types.ID.active_texture_index = IntProperty( - name = "Index for texture_slots", - default = 0, -) +# bpy.types.ID.active_texture_index = IntProperty( + # name = "Index for texture_slots", + # default = 0, +# ) class RenderPovSettingsMaterial(PropertyGroup): """Declare material level properties controllable in UI and translated to POV.""" @@ -1360,6 +1471,7 @@ class RenderPovSettingsMaterial(PropertyGroup): active_texture_index: IntProperty( name = "Index for texture_slots", default = 0, + update = brush_texture_update ) transparency_method: EnumProperty( @@ -2123,7 +2235,7 @@ class MaterialRaytraceTransparency(PropertyGroup): gloss_samples: IntProperty( name="Samples", - description="Number of cone samples averaged for blurry refractions", + description="frequency of the noise sample used for blurry refractions", min=0, max=1024, default=18 ) @@ -2205,8 +2317,8 @@ class MaterialRaytraceMirror(PropertyGroup): ) gloss_samples: IntProperty( - name="Samples", - description="Number of cone samples averaged for blurry reflections", + name="Noise", + description="Frequency of the noise pattern bumps averaged for blurry reflections", min=0, max=1024, default=18, ) @@ -3223,7 +3335,7 @@ class MaterialStrandSettings(PropertyGroup): # Povray Nodes ############################################################################### -class PovraySocketUniversal(bpy.types.NodeSocket): +class PovraySocketUniversal(NodeSocket): bl_idname = 'PovraySocketUniversal' bl_label = 'Povray Socket' value_unlimited: bpy.props.FloatProperty(default=0.0) @@ -3276,7 +3388,7 @@ class PovraySocketUniversal(bpy.types.NodeSocket): def draw_color(self, context, node): return (1, 0, 0, 1) -class PovraySocketFloat_0_1(bpy.types.NodeSocket): +class PovraySocketFloat_0_1(NodeSocket): bl_idname = 'PovraySocketFloat_0_1' bl_label = 'Povray Socket' default_value: bpy.props.FloatProperty(description="Input node Value_0_1",min=0,max=1,default=0) @@ -3289,7 +3401,7 @@ class PovraySocketFloat_0_1(bpy.types.NodeSocket): def draw_color(self, context, node): return (0.5, 0.7, 0.7, 1) -class PovraySocketFloat_0_10(bpy.types.NodeSocket): +class PovraySocketFloat_0_10(NodeSocket): bl_idname = 'PovraySocketFloat_0_10' bl_label = 'Povray Socket' default_value: bpy.props.FloatProperty(description="Input node Value_0_10",min=0,max=10,default=0) @@ -3304,7 +3416,7 @@ class PovraySocketFloat_0_10(bpy.types.NodeSocket): def draw_color(self, context, node): return (0.65, 0.65, 0.65, 1) -class PovraySocketFloat_10(bpy.types.NodeSocket): +class PovraySocketFloat_10(NodeSocket): bl_idname = 'PovraySocketFloat_10' bl_label = 'Povray Socket' default_value: bpy.props.FloatProperty(description="Input node Value_10",min=-10,max=10,default=0) @@ -3319,7 +3431,7 @@ class PovraySocketFloat_10(bpy.types.NodeSocket): def draw_color(self, context, node): return (0.65, 0.65, 0.65, 1) -class PovraySocketFloatPositive(bpy.types.NodeSocket): +class PovraySocketFloatPositive(NodeSocket): bl_idname = 'PovraySocketFloatPositive' bl_label = 'Povray Socket' default_value: bpy.props.FloatProperty(description="Input Node Value Positive", min=0.0, default=0) @@ -3331,7 +3443,7 @@ class PovraySocketFloatPositive(bpy.types.NodeSocket): def draw_color(self, context, node): return (0.045, 0.005, 0.136, 1) -class PovraySocketFloat_000001_10(bpy.types.NodeSocket): +class PovraySocketFloat_000001_10(NodeSocket): bl_idname = 'PovraySocketFloat_000001_10' bl_label = 'Povray Socket' default_value: bpy.props.FloatProperty(min=0.000001,max=10,default=0.000001) @@ -3343,7 +3455,7 @@ class PovraySocketFloat_000001_10(bpy.types.NodeSocket): def draw_color(self, context, node): return (1, 0, 0, 1) -class PovraySocketFloatUnlimited(bpy.types.NodeSocket): +class PovraySocketFloatUnlimited(NodeSocket): bl_idname = 'PovraySocketFloatUnlimited' bl_label = 'Povray Socket' default_value: bpy.props.FloatProperty(default = 0.0) @@ -3355,7 +3467,7 @@ class PovraySocketFloatUnlimited(bpy.types.NodeSocket): def draw_color(self, context, node): return (0.7, 0.7, 1, 1) -class PovraySocketInt_1_9(bpy.types.NodeSocket): +class PovraySocketInt_1_9(NodeSocket): bl_idname = 'PovraySocketInt_1_9' bl_label = 'Povray Socket' default_value: bpy.props.IntProperty(description="Input node Value_1_9",min=1,max=9,default=6) @@ -3367,7 +3479,7 @@ class PovraySocketInt_1_9(bpy.types.NodeSocket): def draw_color(self, context, node): return (1, 0.7, 0.7, 1) -class PovraySocketInt_0_256(bpy.types.NodeSocket): +class PovraySocketInt_0_256(NodeSocket): bl_idname = 'PovraySocketInt_0_256' bl_label = 'Povray Socket' default_value: bpy.props.IntProperty(min=0,max=255,default=0) @@ -3380,7 +3492,7 @@ class PovraySocketInt_0_256(bpy.types.NodeSocket): return (0.5, 0.5, 0.5, 1) -class PovraySocketPattern(bpy.types.NodeSocket): +class PovraySocketPattern(NodeSocket): bl_idname = 'PovraySocketPattern' bl_label = 'Povray Socket' @@ -3417,7 +3529,7 @@ class PovraySocketPattern(bpy.types.NodeSocket): def draw_color(self, context, node): return (1, 1, 1, 1) -class PovraySocketColor(bpy.types.NodeSocket): +class PovraySocketColor(NodeSocket): bl_idname = 'PovraySocketColor' bl_label = 'Povray Socket' @@ -3434,7 +3546,7 @@ class PovraySocketColor(bpy.types.NodeSocket): def draw_color(self, context, node): return (1, 1, 0, 1) -class PovraySocketColorRGBFT(bpy.types.NodeSocket): +class PovraySocketColorRGBFT(NodeSocket): bl_idname = 'PovraySocketColorRGBFT' bl_label = 'Povray Socket' @@ -3452,7 +3564,7 @@ class PovraySocketColorRGBFT(bpy.types.NodeSocket): def draw_color(self, context, node): return (1, 1, 0, 1) -class PovraySocketTexture(bpy.types.NodeSocket): +class PovraySocketTexture(NodeSocket): bl_idname = 'PovraySocketTexture' bl_label = 'Povray Socket' default_value: bpy.props.IntProperty() @@ -3464,7 +3576,7 @@ class PovraySocketTexture(bpy.types.NodeSocket): -class PovraySocketTransform(bpy.types.NodeSocket): +class PovraySocketTransform(NodeSocket): bl_idname = 'PovraySocketTransform' bl_label = 'Povray Socket' default_value: bpy.props.IntProperty(min=0,max=255,default=0) @@ -3474,7 +3586,7 @@ class PovraySocketTransform(bpy.types.NodeSocket): def draw_color(self, context, node): return (99/255, 99/255, 199/255, 1) -class PovraySocketNormal(bpy.types.NodeSocket): +class PovraySocketNormal(NodeSocket): bl_idname = 'PovraySocketNormal' bl_label = 'Povray Socket' default_value: bpy.props.IntProperty(min=0,max=255,default=0) @@ -3484,7 +3596,7 @@ class PovraySocketNormal(bpy.types.NodeSocket): def draw_color(self, context, node): return (0.65, 0.65, 0.65, 1) -class PovraySocketSlope(bpy.types.NodeSocket): +class PovraySocketSlope(NodeSocket): bl_idname = 'PovraySocketSlope' bl_label = 'Povray Socket' default_value: bpy.props.FloatProperty(min = 0.0, max = 1.0) @@ -3500,7 +3612,7 @@ class PovraySocketSlope(bpy.types.NodeSocket): def draw_color(self, context, node): return (0, 0, 0, 1) -class PovraySocketMap(bpy.types.NodeSocket): +class PovraySocketMap(NodeSocket): bl_idname = 'PovraySocketMap' bl_label = 'Povray Socket' default_value: bpy.props.StringProperty() @@ -3774,7 +3886,7 @@ class RenderPovSettingsTexture(PropertyGroup): ('checker', "Checker", "", 'PLUGIN', 34), ('hexagon', "Hexagon", "", 'PLUGIN', 35), ('object', "Mesh", "", 'PLUGIN', 36), - ('emulator', "Internal Emulator", "", 'PLUG', 37) + ('emulator', "Blender Type Emulator", "", 'SCRIPTPLUGINS', 37) ), default='emulator', ) @@ -5167,7 +5279,7 @@ class RenderPovSettingsWorld(PropertyGroup): items=( ('MATERIAL', "", "Show material textures", "MATERIAL",0), # "Show material textures" ('WORLD', "", "Show world textures", "WORLD",1), # "Show world textures" - ('LAMP', "", "Show lamp textures", "LIGHT",2), # "Show lamp textures" + ('LIGHT', "", "Show lamp textures", "LIGHT",2), # "Show lamp textures" ('PARTICLES', "", "Show particles textures", "PARTICLES",3), # "Show particles textures" ('LINESTYLE', "", "Show linestyle textures", "LINE_DATA",4), # "Show linestyle textures" ('OTHER', "", "Show other data textures", "TEXTURE_DATA",5), # "Show other data textures" @@ -5215,12 +5327,27 @@ class RenderPovSettingsWorld(PropertyGroup): ) active_texture_index: IntProperty( name = "Index for texture_slots", - default = 0 + default = 0, + update = brush_texture_update ) - class WorldTextureSlot(PropertyGroup): - """Declare world texture slot properties controllable in UI and translated to POV.""" + """Declare world texture slot level properties for UI and translated to POV.""" + + bl_idname="pov_texture_slots", + bl_description="Texture_slots from Blender-2.79", + + # Adding a "real" texture datablock as property is not possible + # (or at least not easy through a dynamically populated EnumProperty). + # That's why we'll use a prop_search() UILayout function in ui.py. + # So we'll assign the name of the needed texture datablock to the below StringProperty. + texture : StringProperty(update=active_texture_name_from_uilist) + # and use another temporary StringProperty to change the linked data + texture_search : StringProperty( + name="", + update = active_texture_name_from_search, + description = "Browse Texture to be linked", + ) blend_factor: FloatProperty( name="Blend", @@ -5241,6 +5368,31 @@ class WorldTextureSlot(PropertyGroup): default="", ) + offset: FloatVectorProperty( + name="Offset", + description=("Fine tune of the texture mapping X, Y and Z locations "), + precision=4, + step=0.1, + soft_min= -100.0, + soft_max=100.0, + default=(0.0,0.0,0.0), + options={'ANIMATABLE'}, + subtype='TRANSLATION', + ) + + scale: FloatVectorProperty( + name="Size", + subtype='XYZ', + size=3, + description="Set scaling for the texture’s X, Y and Z sizes ", + precision=4, + step=0.1, + soft_min= -100.0, + soft_max=100.0, + default=(1.0,1.0,1.0), + options={'ANIMATABLE'}, + ) + texture_coords: EnumProperty( name="Coordinates", description="Texture coordinates used to map the texture onto the background", @@ -5308,7 +5460,7 @@ for i in range(18): # length of world texture slots class MATERIAL_TEXTURE_SLOTS_UL_POV_layerlist(bpy.types.UIList): # texture_slots: - index: bpy.props.IntProperty(name='index') + #index: bpy.props.IntProperty(name='index') # foo = random prop def draw_item(self, context, layout, data, item, icon, active_data, active_propname): ob = data @@ -5385,11 +5537,44 @@ class PovrayPreferences(AddonPreferences): subtype='FILE_PATH', ) + use_sounds: BoolProperty( + name="Use Sound", + description="Signaling end of the render process at various" + "stages can help if you're away from monitor", + default=False, + ) + + # TODO: Auto find POV sound directory as it does for binary + # And implement the three cases, left uncommented for a dummy + # interface in case some doc screenshots get made for that area + filepath_complete_sound: StringProperty( + name="Finish Render Sound", + description="Path to finished render sound file", + subtype='FILE_PATH', + ) + + filepath_parse_error_sound: StringProperty( + name="Parse Error Sound", + description="Path to parsing time error sound file", + subtype='FILE_PATH', + ) + + filepath_cancel_sound: StringProperty( + name="Cancel Render Sound", + description="Path to cancelled or render time error sound file", + subtype='FILE_PATH', + ) + + #shall we not move this to UI file? def draw(self, context): layout = self.layout layout.prop(self, "branch_feature_set_povray") layout.prop(self, "filepath_povray") layout.prop(self, "docpath_povray") + layout.prop(self, "filepath_complete_sound") + layout.prop(self, "filepath_parse_error_sound") + layout.prop(self, "filepath_cancel_sound") + layout.prop(self, "use_sounds", icon='SOUND') classes = ( @@ -5451,7 +5636,7 @@ def register(): bpy.types.Light.pov = PointerProperty(type=RenderPovSettingsLight) bpy.types.World.pov = PointerProperty(type=RenderPovSettingsWorld) bpy.types.Material.pov_texture_slots = CollectionProperty(type=MaterialTextureSlot) - bpy.types.World.texture_slots = CollectionProperty(type=WorldTextureSlot) + bpy.types.World.pov_texture_slots = CollectionProperty(type=WorldTextureSlot) bpy.types.Text.pov = PointerProperty(type=RenderPovSettingsText) @@ -5468,6 +5653,7 @@ def unregister(): del bpy.types.Camera.pov del bpy.types.Light.pov del bpy.types.World.pov + del bpy.types.World.pov_texture_slots del bpy.types.Material.pov_texture_slots del bpy.types.Text.pov diff --git a/render_povray/nodes.py b/render_povray/nodes.py index be535db3..bbdb9754 100644 --- a/render_povray/nodes.py +++ b/render_povray/nodes.py @@ -21,7 +21,14 @@ import bpy from bpy.utils import register_class -from bpy.types import Node, ShaderNodeTree, CompositorNodeTree, TextureNodeTree#, NodeSocket +from bpy.types import ( + Node, + ShaderNodeTree, + CompositorNodeTree, + TextureNodeTree, + #NodeSocket, + Operator, + ) from bpy.props import ( StringProperty, BoolProperty, @@ -1025,7 +1032,7 @@ class TextureOutputNode(Node, TextureNodeTree): ################################################################################## -class NODE_OT_iso_add(bpy.types.Operator): +class NODE_OT_iso_add(Operator): bl_idname = "pov.nodeisoadd" bl_label = "Create iso props" @@ -1042,7 +1049,7 @@ class NODE_OT_iso_add(bpy.types.Operator): isonode.label = ob.name return {'FINISHED'} -class NODE_OT_map_create(bpy.types.Operator): +class NODE_OT_map_create(Operator): bl_idname = "node.map_create" bl_label = "Create map" @@ -1067,7 +1074,7 @@ class NODE_OT_map_create(bpy.types.Operator): mat = context.object.active_material layout.prop(mat.pov,"inputs_number") -class NODE_OT_povray_node_texture_map_add(bpy.types.Operator): +class NODE_OT_povray_node_texture_map_add(Operator): bl_idname = "pov.nodetexmapadd" bl_label = "Texture map" @@ -1091,7 +1098,7 @@ class NODE_OT_povray_node_texture_map_add(bpy.types.Operator): return {'FINISHED'} -class NODE_OT_povray_node_output_add(bpy.types.Operator): +class NODE_OT_povray_node_output_add(Operator): bl_idname = "pov.nodeoutputadd" bl_label = "Output" @@ -1105,7 +1112,7 @@ class NODE_OT_povray_node_output_add(bpy.types.Operator): tmap.label="Output" return {'FINISHED'} -class NODE_OT_povray_node_layered_add(bpy.types.Operator): +class NODE_OT_povray_node_layered_add(Operator): bl_idname = "pov.nodelayeredadd" bl_label = "Layered material" @@ -1116,7 +1123,7 @@ class NODE_OT_povray_node_layered_add(bpy.types.Operator): tmap.label="Layered material" return {'FINISHED'} -class NODE_OT_povray_input_add(bpy.types.Operator): +class NODE_OT_povray_input_add(Operator): bl_idname = "pov.nodeinputadd" bl_label = "Add entry" @@ -1141,7 +1148,7 @@ class NODE_OT_povray_input_add(bpy.types.Operator): return {'FINISHED'} -class NODE_OT_povray_input_remove(bpy.types.Operator): +class NODE_OT_povray_input_remove(Operator): bl_idname = "pov.nodeinputremove" bl_label = "Remove input" @@ -1159,7 +1166,7 @@ class NODE_OT_povray_input_remove(bpy.types.Operator): els.remove(el) return {'FINISHED'} -class NODE_OT_povray_image_open(bpy.types.Operator): +class NODE_OT_povray_image_open(Operator): bl_idname = "pov.imageopen" bl_label = "Open" @@ -1181,7 +1188,7 @@ class NODE_OT_povray_image_open(bpy.types.Operator): return {'FINISHED'} -# class TEXTURE_OT_povray_open_image(bpy.types.Operator): +# class TEXTURE_OT_povray_open_image(Operator): # bl_idname = "pov.openimage" # bl_label = "Open Image" @@ -1204,7 +1211,7 @@ class NODE_OT_povray_image_open(bpy.types.Operator): # view_layer.update() # return {'FINISHED'} -class PovrayPatternNode(bpy.types.Operator): +class PovrayPatternNode(Operator): bl_idname = "pov.patternnode" bl_label = "Pattern" @@ -1259,7 +1266,7 @@ class PovrayPatternNode(bpy.types.Operator): context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} -class UpdatePreviewMaterial(bpy.types.Operator): +class UpdatePreviewMaterial(Operator): '''Operator update preview material''' bl_idname = "node.updatepreview" bl_label = "Update preview" @@ -1283,7 +1290,7 @@ class UpdatePreviewMaterial(bpy.types.Operator): context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} -class UpdatePreviewKey(bpy.types.Operator): +class UpdatePreviewKey(Operator): '''Operator update preview keymap''' bl_idname = "wm.updatepreviewkey" bl_label = "Activate RMB" diff --git a/render_povray/primitives.py b/render_povray/primitives.py index a9d68d44..6d864220 100644 --- a/render_povray/primitives.py +++ b/render_povray/primitives.py @@ -26,7 +26,7 @@ from bpy_extras.io_utils import ImportHelper from bpy_extras import object_utils from bpy.utils import register_class from math import atan, pi, degrees, sqrt, cos, sin - +from bpy.types import Operator from bpy.props import ( StringProperty, @@ -41,6 +41,7 @@ from bpy.props import ( from mathutils import Vector, Matrix + # import collections @@ -60,7 +61,7 @@ def pov_define_mesh(mesh, verts, edges, faces, name, hide_geometry=True): return mesh -class POVRAY_OT_lathe_add(bpy.types.Operator): +class POVRAY_OT_lathe_add(Operator): """Add the representation of POV lathe using a screw modifier.""" bl_idname = "pov.addlathe" @@ -212,7 +213,7 @@ def pov_superellipsoid_define(context, op, ob): bpy.ops.object.mode_set(mode="OBJECT") -class POVRAY_OT_superellipsoid_add(bpy.types.Operator): +class POVRAY_OT_superellipsoid_add(Operator): """Add the representation of POV superellipsoid using the pov_superellipsoid_define() function.""" bl_idname = "pov.addsuperellipsoid" @@ -286,7 +287,7 @@ class POVRAY_OT_superellipsoid_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_superellipsoid_update(bpy.types.Operator): +class POVRAY_OT_superellipsoid_update(Operator): """Update the superellipsoid. Delete its previous proxy geometry and rerun pov_superellipsoid_define() function @@ -455,7 +456,7 @@ def pov_supertorus_define(context, op, ob): ob.pov.st_edit = st_edit -class POVRAY_OT_supertorus_add(bpy.types.Operator): +class POVRAY_OT_supertorus_add(Operator): """Add the representation of POV supertorus using the pov_supertorus_define() function.""" bl_idname = "pov.addsupertorus" @@ -530,7 +531,7 @@ class POVRAY_OT_supertorus_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_supertorus_update(bpy.types.Operator): +class POVRAY_OT_supertorus_update(Operator): """Update the supertorus. Delete its previous proxy geometry and rerun pov_supetorus_define() function @@ -566,7 +567,7 @@ class POVRAY_OT_supertorus_update(bpy.types.Operator): ######################################################################################################### -class POVRAY_OT_loft_add(bpy.types.Operator): +class POVRAY_OT_loft_add(Operator): """Create the representation of POV loft using Blender curves.""" bl_idname = "pov.addloft" @@ -695,7 +696,7 @@ class POVRAY_OT_loft_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_plane_add(bpy.types.Operator): +class POVRAY_OT_plane_add(Operator): """Add the representation of POV infinite plane using just a very big Blender Plane. Flag its primitive type with a specific pov.object_as attribute and lock edit mode @@ -725,7 +726,7 @@ class POVRAY_OT_plane_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_box_add(bpy.types.Operator): +class POVRAY_OT_box_add(Operator): """Add the representation of POV box using a simple Blender mesh cube. Flag its primitive type with a specific pov.object_as attribute and lock edit mode @@ -796,7 +797,7 @@ def pov_cylinder_define(context, op, ob, radius, loc, loc_cap): bpy.ops.object.shade_smooth() -class POVRAY_OT_cylinder_add(bpy.types.Operator): +class POVRAY_OT_cylinder_add(Operator): """Add the representation of POV cylinder using pov_cylinder_define() function. Use imported_cyl_loc when this operator is run by POV importer.""" @@ -846,7 +847,7 @@ class POVRAY_OT_cylinder_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_cylinder_update(bpy.types.Operator): +class POVRAY_OT_cylinder_update(Operator): """Update the POV cylinder. Delete its previous proxy geometry and rerun pov_cylinder_define() function @@ -932,7 +933,7 @@ def pov_sphere_define(context, op, ob, loc): bpy.ops.object.mode_set(mode="OBJECT") -class POVRAY_OT_sphere_add(bpy.types.Operator): +class POVRAY_OT_sphere_add(Operator): """Add the representation of POV sphere using pov_sphere_define() function. Use imported_loc when this operator is run by POV importer.""" @@ -989,7 +990,7 @@ class POVRAY_OT_sphere_add(bpy.types.Operator): # return {'FINISHED'} -class POVRAY_OT_sphere_update(bpy.types.Operator): +class POVRAY_OT_sphere_update(Operator): """Update the POV sphere. Delete its previous proxy geometry and rerun pov_sphere_define() function @@ -1084,7 +1085,7 @@ def pov_cone_define(context, op, ob): ob.pov.cone_cap_z = zc -class POVRAY_OT_cone_add(bpy.types.Operator): +class POVRAY_OT_cone_add(Operator): """Add the representation of POV cone using pov_cone_define() function.""" bl_idname = "pov.cone_add" @@ -1139,7 +1140,7 @@ class POVRAY_OT_cone_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_cone_update(bpy.types.Operator): +class POVRAY_OT_cone_update(Operator): """Update the POV cone. Delete its previous proxy geometry and rerun pov_cone_define() function @@ -1177,7 +1178,7 @@ class POVRAY_OT_cone_update(bpy.types.Operator): ########################################ISOSURFACES################################## -class POVRAY_OT_isosurface_box_add(bpy.types.Operator): +class POVRAY_OT_isosurface_box_add(Operator): """Add the representation of POV isosurface box using also just a Blender mesh cube. Flag its primitive type with a specific pov.object_as attribute and lock edit mode @@ -1207,7 +1208,7 @@ class POVRAY_OT_isosurface_box_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_isosurface_sphere_add(bpy.types.Operator): +class POVRAY_OT_isosurface_sphere_add(Operator): """Add the representation of POV isosurface sphere by a Blender mesh icosphere. Flag its primitive type with a specific pov.object_as attribute and lock edit mode @@ -1238,7 +1239,7 @@ class POVRAY_OT_isosurface_sphere_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_sphere_sweep_add(bpy.types.Operator): +class POVRAY_OT_sphere_sweep_add(Operator): """Add the representation of POV sphere_sweep using a Blender NURBS curve. Flag its primitive type with a specific ob.pov.curveshape attribute and @@ -1264,7 +1265,7 @@ class POVRAY_OT_sphere_sweep_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_blob_add(bpy.types.Operator): +class POVRAY_OT_blob_add(Operator): """Add the representation of POV blob using a Blender meta ball. No need to flag its primitive type as meta are exported to blobs @@ -1284,7 +1285,7 @@ class POVRAY_OT_blob_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_rainbow_add(bpy.types.Operator): +class POVRAY_OT_rainbow_add(Operator): """Add the representation of POV rainbow using a Blender spot light. Rainbows indeed propagate along a visibility cone. @@ -1334,7 +1335,7 @@ class POVRAY_OT_height_field_add(bpy.types.Operator, ImportHelper): bl_idname = "pov.addheightfield" bl_label = "Height Field" - bl_description = "Add Height Field " + bl_description = "Add Height Field" bl_options = {'REGISTER', 'UNDO'} # XXX Keep it in sync with __init__'s hf Primitive @@ -1470,7 +1471,7 @@ def pov_torus_define(context, op, ob): bpy.ops.object.mode_set(mode="OBJECT") -class POVRAY_OT_torus_add(bpy.types.Operator): +class POVRAY_OT_torus_add(Operator): """Add the representation of POV torus using using pov_torus_define() function.""" bl_idname = "pov.addtorus" @@ -1503,7 +1504,7 @@ class POVRAY_OT_torus_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_torus_update(bpy.types.Operator): +class POVRAY_OT_torus_update(Operator): """Update the POV torus. Delete its previous proxy geometry and rerun pov_torus_define() function @@ -1536,7 +1537,7 @@ class POVRAY_OT_torus_update(bpy.types.Operator): ################################################################################### -class POVRAY_OT_prism_add(bpy.types.Operator): +class POVRAY_OT_prism_add(Operator): """Add the representation of POV prism using using an extruded curve.""" bl_idname = "pov.addprism" @@ -1662,7 +1663,7 @@ def pov_parametric_define(context, op, ob): bpy.ops.object.mode_set(mode="OBJECT") -class POVRAY_OT_parametric_add(bpy.types.Operator): +class POVRAY_OT_parametric_add(Operator): """Add the representation of POV parametric surfaces using pov_parametric_define() function.""" bl_idname = "pov.addparametric" @@ -1698,7 +1699,7 @@ class POVRAY_OT_parametric_add(bpy.types.Operator): return {'FINISHED'} -class POVRAY_OT_parametric_update(bpy.types.Operator): +class POVRAY_OT_parametric_update(Operator): """Update the representation of POV parametric surfaces. Delete its previous proxy geometry and rerun pov_parametric_define() function @@ -1731,7 +1732,7 @@ class POVRAY_OT_parametric_update(bpy.types.Operator): ####################################################################### -class POVRAY_OT_shape_polygon_to_circle_add(bpy.types.Operator): +class POVRAY_OT_shape_polygon_to_circle_add(Operator): """Add the proxy mesh for POV Polygon to circle lofting macro""" bl_idname = "pov.addpolygontocircle" diff --git a/render_povray/render.py b/render_povray/render.py index 6efe7291..f9de22e4 100644 --- a/render_povray/render.py +++ b/render_povray/render.py @@ -24,6 +24,10 @@ import os import sys import time from math import atan, pi, degrees, sqrt, cos, sin +#################### +## Faster mesh export +import numpy as np +#################### import re import random import platform # @@ -69,6 +73,7 @@ def imageFormat(imgF): def imgMap(ts): """Translate mapping type from Blender UI to POV syntax and return that string.""" image_map = "" + texdata = bpy.data.textures[ts.texture] if ts.mapping == 'FLAT': image_map = "map_type 0 " elif ts.mapping == 'SPHERE': @@ -82,9 +87,9 @@ def imgMap(ts): # image_map = " map_type 3 " # elif ts.mapping=="?": # image_map = " map_type 4 " - if ts.texture.use_interpolation: + if ts.use_interpolation: # Available if image sampling class reactivated? image_map += " interpolate 2 " - if ts.texture.extension == 'CLIP': + if texdata.extension == 'CLIP': image_map += " once " # image_map += "}" # if ts.mapping=='CUBE': @@ -110,12 +115,12 @@ def imgMapTransforms(ts): image_map_transforms = ( "scale <%.4g,%.4g,%.4g> translate <%.4g,%.4g,%.4g>" % ( - 1.0 / ts.scale.x, - 1.0 / ts.scale.y, - 1.0 / ts.scale.z, - 0.5 - (0.5 / ts.scale.x) - (ts.offset.x), - 0.5 - (0.5 / ts.scale.y) - (ts.offset.y), - ts.offset.z, + ts.scale[0], + ts.scale[1], + ts.scale[2], + ts.offset[0], + ts.offset[1], + ts.offset[2], ) ) # image_map_transforms = (" translate <-0.5,-0.5,0.0> scale <%.4g,%.4g,%.4g> translate <%.4g,%.4g,%.4g>" % \ @@ -137,6 +142,7 @@ def imgMapTransforms(ts): def imgMapBG(wts): """Translate world mapping from Blender UI to POV syntax and return that string.""" + tex = bpy.data.textures[wts.texture] image_mapBG = "" # texture_coords refers to the mapping of world textures: if wts.texture_coords == 'VIEW' or wts.texture_coords == 'GLOBAL': @@ -146,9 +152,9 @@ def imgMapBG(wts): elif wts.texture_coords == 'TUBE': image_mapBG = " map_type 2 " - if wts.texture.use_interpolation: + if tex.use_interpolation: image_mapBG += " interpolate 2 " - if wts.texture.extension == 'CLIP': + if tex.extension == 'CLIP': image_mapBG += " once " # image_mapBG += "}" # if wts.mapping == 'CUBE': @@ -386,6 +392,7 @@ def write_object_modifiers(scene, ob, File): def write_pov(filename, scene=None, info_callback=None): """Main export process from Blender UI to POV syntax and write to exported file """ + import mathutils # file = filename @@ -690,7 +697,7 @@ def write_pov(filename, scene=None, info_callback=None): tabWrite("right <%s, 0, 0>\n" % -Qsize) tabWrite("up <0, 1, 0>\n") tabWrite( - "angle %f\n" % (360.0 * atan(16.0 / camera.data.lens) / pi) + "angle %f\n" % ( 2 * atan(camera.data.sensor_width / 2 / camera.data.lens) * 180.0 / pi ) ) tabWrite( @@ -737,6 +744,7 @@ def write_pov(filename, scene=None, info_callback=None): def exportLamps(lamps): """Translate lights from Blender UI to POV syntax and write to exported file.""" + # Incremented after each lamp export to declare its target # currently used for Fresnel diffuse shader as their slope vector: global lampCount @@ -2346,6 +2354,64 @@ def write_pov(filename, scene=None, info_callback=None): def exportMeshes(scene, sel, csg): """write all meshes as POV mesh2{} syntax to exported file """ + #some numpy functions to speed up mesh export + + # TODO: also write a numpy function to read matrices at object level? + # feed below with mesh object.data, but only after doing data.calc_loop_triangles() + def read_verts_co(self, mesh): + #'float64' would be a slower 64-bit floating-point number numpy datatype + # using 'float32' vert coordinates for now until any issue is reported + mverts_co = np.zeros((len(mesh.vertices)*3), dtype=np.float32) + mesh.vertices.foreach_get("co", mverts_co) + return np.reshape(mverts_co, (len(mesh.vertices), 3)) + + def read_verts_idx(self, mesh): + mverts_idx = np.zeros((len(mesh.vertices)), dtype=np.int64) + mesh.vertices.foreach_get("index", mverts_idx) + return np.reshape(mverts_idx, (len(mesh.vertices), 1)) + + def read_verts_norms(self, mesh): + #'float64' would be a slower 64-bit floating-point number numpy datatype + # using less accurate 'float16' normals for now until any issue is reported + mverts_no = np.zeros((len(mesh.vertices)*3), dtype=np.float16) + mesh.vertices.foreach_get("normal", mverts_no) + return np.reshape(mverts_no, (len(mesh.vertices), 3)) + + def read_faces_idx(self, mesh): + mfaces_idx = np.zeros((len(mesh.loop_triangles)), dtype=np.int64) + mesh.loop_triangles.foreach_get("index", mfaces_idx) + return np.reshape(mfaces_idx, (len(mesh.loop_triangles), 1)) + + def read_faces_verts_indices(self, mesh): + mfaces_verts_idx = np.zeros((len(mesh.loop_triangles)*3), dtype=np.int64) + mesh.loop_triangles.foreach_get("vertices", mfaces_verts_idx) + return np.reshape(mfaces_verts_idx, (len(mesh.loop_triangles), 3)) + + #Why is below different from verex indices? + def read_faces_verts_loops(self, mesh): + mfaces_verts_loops = np.zeros((len(mesh.loop_triangles)*3), dtype=np.int64) + mesh.loop_triangles.foreach_get("loops", mfaces_verts_loops) + return np.reshape(mfaces_verts_loops, (len(mesh.loop_triangles), 3)) + + def read_faces_norms(self, mesh): + #'float64' would be a slower 64-bit floating-point number numpy datatype + # using less accurate 'float16' normals for now until any issue is reported + mfaces_no = np.zeros((len(mesh.loop_triangles)*3), dtype=np.float16) + mesh.loop_triangles.foreach_get("normal", mfaces_no) + return np.reshape(mfaces_no, (len(mesh.loop_triangles), 3)) + + def read_faces_smooth(self, mesh): + mfaces_smth = np.zeros((len(mesh.loop_triangles)*1), dtype=np.bool) + mesh.loop_triangles.foreach_get("use_smooth", mfaces_smth) + return np.reshape(mfaces_smth, (len(mesh.loop_triangles), 1)) + + def read_faces_material_indices(self, mesh): + mfaces_mats_idx = np.zeros((len(mesh.loop_triangles)), dtype=np.int16) + mesh.loop_triangles.foreach_get("material_index", mfaces_mats_idx) + return np.reshape(mfaces_mats_idx, (len(mesh.loop_triangles), 1)) + + + # obmatslist = [] # def hasUniqueMaterial(): # # Grab materials attached to object instances ... @@ -2684,6 +2750,7 @@ def write_pov(filename, scene=None, info_callback=None): pmaterial = ob.material_slots[ pSys.settings.material - 1 ].material + #XXX Todo: replace by pov_(Particles?)_texture_slot for th in pmaterial.texture_slots: if th and th.use: if ( @@ -3411,6 +3478,8 @@ def write_pov(filename, scene=None, info_callback=None): ob.pov.v_max, ) ) + # Previous to 3.8 default max_gradient 1.0 was too slow + tabWrite("max_gradient 0.001\n") if ob.pov.contained_by == "sphere": tabWrite("contained_by { sphere{0, 2} }\n") else: @@ -3593,6 +3662,14 @@ def write_pov(filename, scene=None, info_callback=None): me.calc_loop_triangles() me_materials = me.materials me_faces = me.loop_triangles[:] + ## numpytest + #me_looptris = me.loops + + ## otypes = ['int32'] is a 32-bit signed integer number numpy datatype + #get_v_index = np.vectorize(lambda l: l.vertex_index, otypes = ['int32'], cache = True) + #faces_verts_idx = get_v_index(me_looptris) + + # if len(me_faces)==0: # tabWrite("\n//dummy sphere to represent empty mesh location\n") # tabWrite("#declare %s =sphere {<0, 0, 0>,0 pigment{rgbt 1} no_image no_reflection no_radiosity photons{pass_through collect off} hollow}\n" % povdataname) @@ -4092,6 +4169,7 @@ def write_pov(filename, scene=None, info_callback=None): else: shading.writeTextureInfluence( + using_uberpov, mater, materialNames, LocalMaterialNames, @@ -4581,89 +4659,91 @@ def write_pov(filename, scene=None, info_callback=None): for ( t ) in ( - world.texture_slots + world.pov_texture_slots ): # risk to write several sky_spheres but maybe ok. - if t and t.texture.type is not None: + if t: + tex = bpy.data.textures[t.texture] + if tex.type is not None: worldTexCount += 1 - # XXX No enable checkbox for world textures yet (report it?) - # if t and t.texture.type == 'IMAGE' and t.use: - if t and t.texture.type == 'IMAGE': - image_filename = path_image(t.texture.image) - if t.texture.image.filepath != image_filename: - t.texture.image.filepath = image_filename - if image_filename != "" and t.use_map_blend: - texturesBlend = image_filename - # colvalue = t.default_value - t_blend = t - - # Commented below was an idea to make the Background image oriented as camera - # taken here: - # http://news.pov.org/pov.newusers/thread/%3Cweb.4a5cddf4e9c9822ba2f93e20@news.pov.org%3E/ - # Replace 4/3 by the ratio of each image found by some custom or existing - # function - # mappingBlend = (" translate <%.4g,%.4g,%.4g> rotate z*degrees" \ - # "(atan((camLocation - camLookAt).x/(camLocation - " \ - # "camLookAt).y)) rotate x*degrees(atan((camLocation - " \ - # "camLookAt).y/(camLocation - camLookAt).z)) rotate y*" \ - # "degrees(atan((camLocation - camLookAt).z/(camLocation - " \ - # "camLookAt).x)) scale <%.4g,%.4g,%.4g>b" % \ - # (t_blend.offset.x / 10 , t_blend.offset.y / 10 , - # t_blend.offset.z / 10, t_blend.scale.x , - # t_blend.scale.y , t_blend.scale.z)) - # using camera rotation valuesdirectly from blender seems much easier - if t_blend.texture_coords == 'ANGMAP': - mappingBlend = "" - else: - # POV-Ray "scale" is not a number of repetitions factor, but its - # inverse, a standard scale factor. - # 0.5 Offset is needed relatively to scale because center of the - # UV scale is 0.5,0.5 in blender and 0,0 in POV - # Further Scale by 2 and translate by -1 are - # required for the sky_sphere not to repeat - - mappingBlend = ( - "scale 2 scale <%.4g,%.4g,%.4g> translate -1 " - "translate <%.4g,%.4g,%.4g> rotate<0,0,0> " + # XXX No enable checkbox for world textures yet (report it?) + # if t and tex.type == 'IMAGE' and t.use: + if tex.type == 'IMAGE': + image_filename = path_image(tex.image) + if tex.image.filepath != image_filename: + tex.image.filepath = image_filename + if image_filename != "" and t.use_map_blend: + texturesBlend = image_filename + # colvalue = t.default_value + t_blend = t + + # Commented below was an idea to make the Background image oriented as camera + # taken here: + # http://news.pov.org/pov.newusers/thread/%3Cweb.4a5cddf4e9c9822ba2f93e20@news.pov.org%3E/ + # Replace 4/3 by the ratio of each image found by some custom or existing + # function + # mappingBlend = (" translate <%.4g,%.4g,%.4g> rotate z*degrees" \ + # "(atan((camLocation - camLookAt).x/(camLocation - " \ + # "camLookAt).y)) rotate x*degrees(atan((camLocation - " \ + # "camLookAt).y/(camLocation - camLookAt).z)) rotate y*" \ + # "degrees(atan((camLocation - camLookAt).z/(camLocation - " \ + # "camLookAt).x)) scale <%.4g,%.4g,%.4g>b" % \ + # (t_blend.offset.x / 10 , t_blend.offset.y / 10 , + # t_blend.offset.z / 10, t_blend.scale.x , + # t_blend.scale.y , t_blend.scale.z)) + # using camera rotation valuesdirectly from blender seems much easier + if t_blend.texture_coords == 'ANGMAP': + mappingBlend = "" + else: + # POV-Ray "scale" is not a number of repetitions factor, but its + # inverse, a standard scale factor. + # 0.5 Offset is needed relatively to scale because center of the + # UV scale is 0.5,0.5 in blender and 0,0 in POV + # Further Scale by 2 and translate by -1 are + # required for the sky_sphere not to repeat + + mappingBlend = ( + "scale 2 scale <%.4g,%.4g,%.4g> translate -1 " + "translate <%.4g,%.4g,%.4g> rotate<0,0,0> " + % ( + (1.0 / t_blend.scale.x), + (1.0 / t_blend.scale.y), + (1.0 / t_blend.scale.z), + 0.5 + - (0.5 / t_blend.scale.x) + - t_blend.offset.x, + 0.5 + - (0.5 / t_blend.scale.y) + - t_blend.offset.y, + t_blend.offset.z, + ) + ) + + # The initial position and rotation of the pov camera is probably creating + # the rotation offset should look into it someday but at least background + # won't rotate with the camera now. + # Putting the map on a plane would not introduce the skysphere distortion and + # allow for better image scale matching but also some waay to chose depth and + # size of the plane relative to camera. + tabWrite("sky_sphere {\n") + tabWrite("pigment {\n") + tabWrite( + "image_map{%s \"%s\" %s}\n" % ( - (1.0 / t_blend.scale.x), - (1.0 / t_blend.scale.y), - (1.0 / t_blend.scale.z), - 0.5 - - (0.5 / t_blend.scale.x) - - t_blend.offset.x, - 0.5 - - (0.5 / t_blend.scale.y) - - t_blend.offset.y, - t_blend.offset.z, + imageFormat(texturesBlend), + texturesBlend, + imgMapBG(t_blend), ) ) - - # The initial position and rotation of the pov camera is probably creating - # the rotation offset should look into it someday but at least background - # won't rotate with the camera now. - # Putting the map on a plane would not introduce the skysphere distortion and - # allow for better image scale matching but also some waay to chose depth and - # size of the plane relative to camera. - tabWrite("sky_sphere {\n") - tabWrite("pigment {\n") - tabWrite( - "image_map{%s \"%s\" %s}\n" - % ( - imageFormat(texturesBlend), - texturesBlend, - imgMapBG(t_blend), + tabWrite("}\n") + tabWrite("%s\n" % (mappingBlend)) + # The following layered pigment opacifies to black over the texture for + # transmit below 1 or otherwise adds to itself + tabWrite( + "pigment {rgb 0 transmit %s}\n" % (tex.intensity) ) - ) - tabWrite("}\n") - tabWrite("%s\n" % (mappingBlend)) - # The following layered pigment opacifies to black over the texture for - # transmit below 1 or otherwise adds to itself - tabWrite( - "pigment {rgb 0 transmit %s}\n" % (t.texture.intensity) - ) - tabWrite("}\n") - # tabWrite("scale 2\n") - # tabWrite("translate -1\n") + tabWrite("}\n") + # tabWrite("scale 2\n") + # tabWrite("translate -1\n") # For only Background gradient @@ -4910,7 +4990,8 @@ def write_pov(filename, scene=None, info_callback=None): "//--Exported with POV-Ray exporter for Blender--\n" "//----------------------------------------------\n\n" ) - file.write("#version 3.7;\n") + file.write("#version 3.7;\n") #Switch below as soon as 3.8 beta gets easy linked + #file.write("#version 3.8;\n") file.write( "#declare Default_texture = texture{pigment {rgb 0.8} " "finish {brilliance 3.8} }\n\n" @@ -5066,7 +5147,11 @@ def write_pov(filename, scene=None, info_callback=None): if comments: file.write("//--Mesh objects--\n") + + #tbefore = time.time() exportMeshes(scene, sel, csg) + #totime = time.time() - tbefore + #print("exportMeshes took" + str(totime)) # What follow used to happen here: # exportCamera() @@ -5097,7 +5182,7 @@ def write_pov_ini( y = int(render.resolution_y * render.resolution_percentage * 0.01) file = open(filename_ini, "w") - file.write("Version=3.7\n") + file.write("Version=3.8\n") # write povray text stream to temporary file of same name with _log suffix # file.write("All_File='%s'\n" % filename_log) # DEBUG.OUT log if none specified: @@ -5797,15 +5882,106 @@ class PovrayRender(bpy.types.RenderEngine): # print(filename_log) #bring the pov log to blender console with proper path? with open( - self._temp_file_log + self._temp_file_log, + encoding='utf-8' ) as f: # The with keyword automatically closes the file when you are done - print(f.read()) + msg = f.read() + #if isinstance(msg, str): + #stdmsg = msg + #decoded = False + #else: + #if type(msg) == bytes: + #stdmsg = msg.split('\n') + #stdmsg = msg.encode('utf-8', "replace") + #stdmsg = msg.encode("utf-8", "replace") + + #stdmsg = msg.decode(encoding) + #decoded = True + #msg.encode('utf-8').decode('utf-8') + print(msg) + # Also print to the interactive console used in POV centric workspace + # To do: get a grip on new line encoding + # and make this a function to be used elsewhere + for win in bpy.context.window_manager.windows: + if win.screen != None: + scr = win.screen + for area in scr.areas: + if area.type == 'CONSOLE': + #context override + #ctx = {'window': win, 'screen': scr, 'area':area}#bpy.context.copy() + ctx = {} + ctx['area'] = area + ctx['region'] = area.regions[-1] + ctx['space_data'] = area.spaces.active + ctx['screen'] = scr#C.screen + ctx['window'] = win + + #bpy.ops.console.banner(ctx, text = "Hello world") + bpy.ops.console.clear_line(ctx) + stdmsg = msg.split('\n') #XXX todo , test and see + for i in stdmsg: + bpy.ops.console.insert(ctx, text = i) self.update_stats("", "") if scene.pov.tempfiles_enable or scene.pov.deletefiles_enable: self._cleanup() + sound_on = bpy.context.preferences.addons[ + __package__ + ].preferences.use_sounds + + if sys.platform[:3] == "win" and sound_on: + # Could not find tts Windows command so playing beeps instead :-) + # "Korobeiniki"(Коробе́йники) + # aka "A-Type" Tetris theme + import winsound + winsound.Beep(494,250) #B + winsound.Beep(370,125) #F + winsound.Beep(392,125) #G + winsound.Beep(440,250) #A + winsound.Beep(392,125) #G + winsound.Beep(370,125) #F# + winsound.Beep(330,275) #E + winsound.Beep(330,125) #E + winsound.Beep(392,125) #G + winsound.Beep(494,275) #B + winsound.Beep(440,125) #A + winsound.Beep(392,125) #G + winsound.Beep(370,275) #F + winsound.Beep(370,125) #F + winsound.Beep(392,125) #G + winsound.Beep(440,250) #A + winsound.Beep(494,250) #B + winsound.Beep(392,250) #G + winsound.Beep(330,350) #E + time.sleep(0.5) + winsound.Beep(440,250) #A + winsound.Beep(440,150) #A + winsound.Beep(523,125) #D8 + winsound.Beep(659,250) #E8 + winsound.Beep(587,125) #D8 + winsound.Beep(523,125) #C8 + winsound.Beep(494,250) #B + winsound.Beep(494,125) #B + winsound.Beep(392,125) #G + winsound.Beep(494,250) #B + winsound.Beep(440,150) #A + winsound.Beep(392,125) #G + winsound.Beep(370,250) #F# + winsound.Beep(370,125) #F# + winsound.Beep(392,125) #G + winsound.Beep(440,250) #A + winsound.Beep(494,250) #B + winsound.Beep(392,250) #G + winsound.Beep(330,300) #E + + #Does Linux support say command? + elif sys.platform[:3] != "win" : + finished_render_message = "\'Render completed\'" + # We don't want the say command to block Python, + # so we add an ampersand after the message + os.system("say %s &" % (finished_render_message)) ################################################################################## #################################Operators######################################## @@ -5833,7 +6009,7 @@ class RenderPovTexturePreview(Operator): outputPrevFile = os.path.join(preview_dir, texPrevName) ##################### ini ########################################## fileIni = open("%s" % iniPrevFile, "w") - fileIni.write('Version=3.7\n') + fileIni.write('Version=3.8\n') fileIni.write('Input_File_Name="%s"\n' % inputPrevFile) fileIni.write('Output_File_Name="%s.png"\n' % outputPrevFile) fileIni.write('Library_Path="%s"\n' % preview_dir) diff --git a/render_povray/shading.py b/render_povray/shading.py index ac1f923f..a3f907dc 100644 --- a/render_povray/shading.py +++ b/render_povray/shading.py @@ -155,7 +155,7 @@ def writeMaterial(using_uberpov, DEF_MAT_NAME, scene, tabWrite, safety, comments elif Level == 1: if (material.pov.specular_shader == 'COOKTORR' or material.pov.specular_shader == 'PHONG'): - tabWrite("phong %.3g\n" % (material.pov.specular_intensity/5)) + tabWrite("phong 0\n")#%.3g\n" % (material.pov.specular_intensity/5)) tabWrite("phong_size %.3g\n" % (material.pov.specular_hardness /3.14)) # POV-Ray 'specular' keyword corresponds to a Blinn model, without the ior. @@ -183,8 +183,10 @@ def writeMaterial(using_uberpov, DEF_MAT_NAME, scene, tabWrite, safety, comments # specular for some values. tabWrite("brilliance %.4g\n" % (1.8 - material.pov.specular_slope * 1.8)) elif Level == 3: - tabWrite("specular %.3g\n" % ((material.pov.specular_intensity*material.pov.specular_color.v)*5)) - tabWrite("roughness %.3g\n" % (1.1/material.pov.specular_hardness)) + # Spec must be Max at Level 3 so that white of mixing texture always shows specularity + # That's why it's multiplied by 255. maybe replace by texture's brightest pixel value? + tabWrite("specular %.3g\n" % ((material.pov.specular_intensity*material.pov.specular_color.v)*(255* slot.specular_factor))) + tabWrite("roughness %.3g\n" % (1/material.pov.specular_hardness)) tabWrite("diffuse %.3g %.3g\n" % (frontDiffuse, backDiffuse)) tabWrite("ambient %.3g\n" % material.pov.ambient) @@ -265,12 +267,12 @@ def writeMaterial(using_uberpov, DEF_MAT_NAME, scene, tabWrite, safety, comments if material: special_texture_found = False - idx = -1 + tmpidx = -1 for t in material.pov_texture_slots: - idx += 1 + tmpidx += 1 # index = material.pov.active_texture_index - slot = material.pov_texture_slots[idx] # [index] - povtex = slot.name + slot = material.pov_texture_slots[tmpidx] # [index] + povtex = slot.texture # slot.name tex = bpy.data.textures[povtex] if t and t.use and tex is not None: @@ -777,7 +779,7 @@ def exportPattern(texture, string_strip_hyphen): return(texStrg) -def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, lampCount, +def writeTextureInfluence(using_uberpov, mater, materialNames, LocalMaterialNames, path_image, lampCount, imageFormat, imgMap, imgMapTransforms, tabWrite, comments, string_strip_hyphen, safety, col, os, preview_dir, unpacked_images): """Translate Blender texture influences to various POV texture tricks and write to pov file.""" @@ -805,14 +807,15 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, texturesNorm = "" texturesAlpha = "" #proceduralFlag=False + tmpidx = -1 for t in mater.pov_texture_slots: - idx = -1 - for t in mater.pov_texture_slots: - idx += 1 - # index = mater.pov.active_texture_index - slot = mater.pov_texture_slots[idx] # [index] - povtex = slot.name - tex = bpy.data.textures[povtex] + + + tmpidx += 1 + # index = mater.pov.active_texture_index + slot = mater.pov_texture_slots[tmpidx] # [index] + povtex = slot.texture # slot.name + tex = bpy.data.textures[povtex] if t and (t.use and (tex is not None)): # 'NONE' ('NONE' type texture is different from no texture covered above) @@ -876,11 +879,13 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, # IMAGE SEQUENCE ENDS imgGamma = "" if image_filename: + texdata = bpy.data.textures[t.texture] if t.use_map_color_diffuse: texturesDif = image_filename # colvalue = t.default_value # UNUSED t_dif = t - if t_dif.texture.pov.tex_gamma_enable: + print (texdata) + if texdata.pov.tex_gamma_enable: imgGamma = (" gamma %.3g " % t_dif.texture.pov.tex_gamma_value) if t.use_map_specular or t.use_map_raymir: texturesSpec = image_filename @@ -913,6 +918,7 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, if mater.pov.replacement_text != "": tabWrite("%s\n" % mater.pov.replacement_text) ################################################################################# + # XXX TODO: replace by new POV MINNAERT rather than aoi if mater.pov.diffuse_shader == 'MINNAERT': tabWrite("\n") tabWrite("aoi\n") @@ -1056,12 +1062,36 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, mappingNor =imgMapTransforms(t_nor) if texturesNorm and texturesNorm.startswith("PAT_"): - tabWrite("normal{function{f%s(x,y,z).grey} bump_size %.4g %s}\n" %(texturesNorm, t_nor.normal_factor, mappingNor)) + tabWrite("normal{function{f%s(x,y,z).grey} bump_size %.4g %s}\n" %(texturesNorm, ( - t_nor.normal_factor * 9.5), mappingNor)) else: - tabWrite("normal {uv_mapping bump_map " \ - "{%s \"%s\" %s bump_size %.4g }%s}\n" % \ + tabWrite("normal {\n") + # XXX TODO: fix and propagate the micro normals reflection blur below to non textured materials + if (mater.pov_raytrace_mirror.use and mater.pov_raytrace_mirror.gloss_factor < 1.0 and not using_uberpov): + tabWrite("average\n") + tabWrite("normal_map{\n") + # 0.5 for entries below means a 50 percent mix + # between the micro normal and user bump map + # order seems indifferent as commutative + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(10/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.1]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.15]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.2]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.25]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.3]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.35]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.4]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.45]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.5]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[1.0 ") # Proceed with user bump... + tabWrite("uv_mapping bump_map " \ + "{%s \"%s\" %s bump_size %.4g }%s" % \ (imageFormat(texturesNorm), texturesNorm, imgMap(t_nor), - t_nor.normal_factor, mappingNor)) + ( - t_nor.normal_factor * 9.5), mappingNor)) + # ...Then close its last entry and the the normal_map itself + if (mater.pov_raytrace_mirror.use and mater.pov_raytrace_mirror.gloss_factor < 1.0 and not using_uberpov): + tabWrite("]}}\n") + else: + tabWrite("]}\n") if texturesSpec != "": tabWrite("]\n") ##################Second index for mapping specular max value############### @@ -1094,6 +1124,35 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, # Level 3 is full specular tabWrite("finish {%s}\n" % (safety(material_finish, Level=3))) + if mater.pov_raytrace_mirror.use and mater.pov_raytrace_mirror.gloss_factor < 1.0 and not using_uberpov: + tabWrite("normal {\n") + tabWrite("average\n") + tabWrite("normal_map{\n") + # 0.5 for entries below means a 50 percent mix + # between the micro normal and user bump map + # order seems indifferent as commutative + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(10/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.1]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.15]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.2]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.25]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.3]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.35]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.4]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.45]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.5]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + #XXX IF USER BUMP_MAP + if texturesNorm != "": + tabWrite("[1.0 ") # Blurry reflection or not Proceed with user bump in either case... + tabWrite("uv_mapping bump_map " \ + "{%s \"%s\" %s bump_size %.4g }%s]\n" % \ + (imageFormat(texturesNorm), texturesNorm, imgMap(t_nor), + ( - t_nor.normal_factor * 9.5), mappingNor)) + # ...Then close the normal_map itself if blurry reflection + if mater.pov_raytrace_mirror.use and mater.pov_raytrace_mirror.gloss_factor < 1.0 and not using_uberpov: + tabWrite("}}\n") + else: + tabWrite("}\n") elif colored_specular_found: # Level 1 is no specular tabWrite("finish {%s}\n" % (safety(material_finish, Level=1))) @@ -1166,11 +1225,36 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, mappingNor =imgMapTransforms(t_nor) if texturesNorm and texturesNorm.startswith("PAT_"): - tabWrite("normal{function{f%s(x,y,z).grey} bump_size %.4g %s}\n" %(texturesNorm, t_nor.normal_factor, mappingNor)) + tabWrite("normal{function{f%s(x,y,z).grey} bump_size %.4g %s}\n" %(texturesNorm, ( - t_nor.normal_factor * 9.5), mappingNor)) else: - tabWrite("normal {uv_mapping bump_map {%s \"%s\" %s bump_size %.4g }%s}\n" % \ + tabWrite("normal {\n") + # XXX TODO: fix and propagate the micro normals reflection blur below to non textured materials + if mater.pov_raytrace_mirror.use and mater.pov_raytrace_mirror.gloss_factor < 1.0 and not using_uberpov: + tabWrite("average\n") + tabWrite("normal_map{\n") + # 0.5 for entries below means a 50 percent mix + # between the micro normal and user bump map + # order seems indifferent as commutative + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(10/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.1]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.15]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.2]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.25]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.3]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.35]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.4]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.45]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[0.025 bumps %.4g scale 0.1*%.4g phase 0.5]\n" %((10/(mater.pov_raytrace_mirror.gloss_factor+0.01)),(1/(mater.pov_raytrace_mirror.gloss_samples+0.001)))) # micronormals blurring + tabWrite("[1.0 ") # Blurry reflection or not Proceed with user bump in either case... + tabWrite("uv_mapping bump_map " \ + "{%s \"%s\" %s bump_size %.4g }%s]\n" % \ (imageFormat(texturesNorm), texturesNorm, imgMap(t_nor), - t_nor.normal_factor, mappingNor)) + ( - t_nor.normal_factor * 9.5), mappingNor)) + # ...Then close the normal_map itself if blurry reflection + if mater.pov_raytrace_mirror.use and mater.pov_raytrace_mirror.gloss_factor < 1.0 and not using_uberpov: + tabWrite("}}\n") + else: + tabWrite("}\n") if texturesSpec != "" and mater.pov.replacement_text == "": tabWrite("]\n") @@ -1201,12 +1285,12 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, # Write another layered texture using invisible diffuse and metallic trick # to emulate colored specular highlights special_texture_found = False - idx = -1 + tmpidx = -1 for t in mater.pov_texture_slots: - idx += 1 + tmpidx += 1 # index = mater.pov.active_texture_index - slot = mater.pov_texture_slots[idx] # [index] - povtex = slot.name + slot = mater.pov_texture_slots[tmpidx] # [index] + povtex = slot.texture # slot.name tex = bpy.data.textures[povtex] if(t and t.use and ((tex.type == 'IMAGE' and tex.image) or tex.type != 'IMAGE') and (t.use_map_specular or t.use_map_raymir)): @@ -1240,7 +1324,7 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, if image_filename: if t.use_map_normal: texturesNorm = image_filename - # colvalue = t.normal_factor/10 # UNUSED + # colvalue = t.normal_factor/10 # UNUSED XXX *-9.5 ! #textNormName=tex.image.name + ".normal" #was the above used? --MR t_nor = t @@ -1248,13 +1332,13 @@ def writeTextureInfluence(mater, materialNames, LocalMaterialNames, path_image, tabWrite("normal{function" \ "{f%s(x,y,z).grey} bump_size %.4g}\n" % \ (texturesNorm, - t_nor.normal_factor)) + ( - t_nor.normal_factor * 9.5))) else: tabWrite("normal {uv_mapping bump_map " \ "{%s \"%s\" %s bump_size %.4g }%s}\n" % \ (imageFormat(texturesNorm), texturesNorm, imgMap(t_nor), - t_nor.normal_factor, + ( - t_nor.normal_factor * 9.5), mappingNor)) tabWrite("}\n") # THEN IT CAN CLOSE LAST LAYER OF TEXTURE diff --git a/render_povray/ui.py b/render_povray/ui.py index e15a9374..9ac79067 100644 --- a/render_povray/ui.py +++ b/render_povray/ui.py @@ -22,11 +22,15 @@ import bpy import sys # really import here and in render.py? import os # really import here and in render.py? +import addon_utils +from time import sleep from os.path import isfile +from bpy.app.handlers import persistent from bl_operators.presets import AddPresetBase from bpy.utils import register_class, unregister_class from bpy.types import ( Operator, + Menu, UIList, Panel, Brush, @@ -43,17 +47,29 @@ from bl_ui import properties_output for member in dir(properties_output): subclass = getattr(properties_output, member) try: - subclass.COMPAT_ENGINES.add('POVRAY') + subclass.COMPAT_ENGINES.add('POVRAY_RENDER') except: pass del properties_output +from bl_ui import properties_freestyle +for member in dir(properties_freestyle): + subclass = getattr(properties_freestyle, member) + try: + if not (subclass.bl_space_type == 'PROPERTIES' + and subclass.bl_context == "render"): + subclass.COMPAT_ENGINES.add('POVRAY_RENDER') + #subclass.bl_parent_id = "RENDER_PT_POV_filter" + except: + pass +del properties_freestyle + from bl_ui import properties_view_layer for member in dir(properties_view_layer): subclass = getattr(properties_view_layer, member) try: - subclass.COMPAT_ENGINES.add('POVRAY') + subclass.COMPAT_ENGINES.add('POVRAY_RENDER') except: pass del properties_view_layer @@ -242,19 +258,139 @@ for member in dir( pass del properties_particle -# Example of wrapping every class 'as is' -from bl_ui import properties_output -for member in dir(properties_output): - subclass = getattr(properties_output, member) - try: - subclass.COMPAT_ENGINES.add('POVRAY_RENDER') - except: - pass -del properties_output + +#Use some delay? +#https://blenderartists.org/t/restrictdata-object-has-no-attribute-scenes-how-to-avoid-it/579885/4 +############# POV-Centric WORSPACE ############# +@persistent +def povCentricWorkspace(dummy): + """Set up a POV centric Workspace if addon was left activate from previous session + + This would bring a ’_RestrictData’ error because UI needs to be fully loaded before + workspace changes so registering this function in bpy.app.handlers is needed. + By default handlers are freed when loading new files, but here we want the handler + to stay running across multiple files as part of this add-on. That is why the the + bpy.app.handlers.persistent decorator is used (@persistent) above. + """ + + #bpy.context.scene.render.engine = 'POVRAY_RENDER' + wsp = bpy.data.workspaces.get('Scripting') + context = bpy.context + if wsp is not None: + new_wsp = bpy.ops.workspace.duplicate({'workspace': wsp}) + bpy.data.workspaces['Scripting.001'].name='POV' + # Already done it would seem, but explicitly make this workspaces the active one + context.window.workspace = bpy.data.workspaces['POV'] + pov_screen = bpy.data.workspaces['POV'].screens[0] + pov_workspace = pov_screen.areas + + + override = bpy.context.copy() + + for area in pov_workspace: + if area.type == 'VIEW_3D': + for region in [r for r in area.regions if r.type == 'WINDOW']: + for space in area.spaces: + if space.type == 'VIEW_3D': + #override['screen'] = pov_screen + override['area'] = area + override['region']= region + #bpy.data.workspaces['POV'].screens[0].areas[6].spaces[0].width = 333 # Read only, how do we set ? + #This has a glitch: + #bpy.ops.screen.area_move(override, x=(area.x + area.width), y=(area.y + 5), delta=100) + #bpy.ops.screen.area_move(override, x=(area.x + 5), y=area.y, delta=-100) + + bpy.ops.screen.space_type_set_or_cycle(override, space_type = 'TEXT_EDITOR') + #bpy.ops.screen.region_scale(override) + #bpy.ops.screen.region_scale() + break + + elif area.type == 'CONSOLE': + for region in [r for r in area.regions if r.type == 'WINDOW']: + for space in area.spaces: + if space.type == 'CONSOLE': + #override['screen'] = pov_screen + override['area'] = area + override['region']= region + bpy.ops.screen.space_type_set_or_cycle(override, space_type = 'INFO') + + break + elif area.type == 'INFO': + for region in [r for r in area.regions if r.type == 'WINDOW']: + for space in area.spaces: + if space.type == 'INFO': + #override['screen'] = pov_screen + override['area'] = area + override['region']= region + bpy.ops.screen.space_type_set_or_cycle(override, space_type = 'CONSOLE') + + break + + elif area.type == 'TEXT_EDITOR': + for region in [r for r in area.regions if r.type == 'WINDOW']: + for space in area.spaces: + if space.type == 'TEXT_EDITOR': + #override['screen'] = pov_screen + override['area'] = area + override['region']= region + #bpy.ops.screen.space_type_set_or_cycle(space_type='VIEW_3D') + #space.type = 'VIEW_3D' + bpy.ops.screen.space_type_set_or_cycle(override, space_type = 'VIEW_3D') + + #bpy.ops.screen.area_join(override, cursor=(area.x, area.y + area.height)) + + break + + + if area.type == 'VIEW_3D': + for region in [r for r in area.regions if r.type == 'WINDOW']: + for space in area.spaces: + if space.type == 'VIEW_3D': + #override['screen'] = pov_screen + override['area'] = area + override['region']= region + bpy.ops.screen.region_quadview(override) + space.region_3d.view_perspective = 'CAMERA' + #bpy.ops.screen.space_type_set_or_cycle(override, space_type = 'TEXT_EDITOR') + #bpy.ops.screen.region_quadview(override) + + + -class WORLD_MT_POV_presets(bpy.types.Menu): + + bpy.data.workspaces.update() + # Already outliners but invert both types + pov_workspace[1].spaces[0].display_mode = 'LIBRARIES' + pov_workspace[3].spaces[0].display_mode = 'VIEW_LAYER' + + ''' + for window in bpy.context.window_manager.windows: + for area in [a for a in window.screen.areas if a.type == 'VIEW_3D']: + for region in [r for r in area.regions if r.type == 'WINDOW']: + context_override = { + 'window': window, + 'screen': window.screen, + 'area': area, + 'region': region, + 'space_data': area.spaces.active, + 'scene': bpy.context.scene + } + bpy.ops.view3d.camera_to_view(context_override) + ''' + + + else: + print("default 'Scripting' workspace needed for POV centric Workspace") + + + + + + + +class WORLD_MT_POV_presets(Menu): bl_label = "World Presets" preset_subdir = "pov/world" preset_operator = "script.execute_preset" @@ -262,7 +398,7 @@ class WORLD_MT_POV_presets(bpy.types.Menu): class WORLD_OT_POV_add_preset(AddPresetBase, Operator): - '''Add a World Preset''' + """Add a World Preset""" bl_idname = "object.world_preset_add" bl_label = "Add World Preset" @@ -324,6 +460,15 @@ def check_add_mesh_extra_objects(): return True return False +def check_render_freestyle_svg(): + """Test if Freestyle SVG Exporter addon is activated + + This addon is currently used to generate the SVG lines file + when Freestyle is enabled alongside POV + """ + if "render_freestyle_svg" in bpy.context.preferences.addons.keys(): + return True + return False def locate_docpath(): """POV can be installed with some include files. @@ -373,31 +518,29 @@ def pov_context_tex_datablock(context): """Texture context type recreated as deprecated in blender 2.8""" idblock = context.brush - if idblock and bpy.context.scene.texture_context == 'OTHER': + if idblock and context.scene.texture_context == 'OTHER': return idblock # idblock = bpy.context.active_object.active_material - idblock = bpy.context.scene.view_layers[ - "View Layer" - ].objects.active.active_material - if idblock: + idblock = context.view_layer.objects.active.active_material + if idblock and context.scene.texture_context == 'MATERIAL': return idblock - idblock = context.world - if idblock: + idblock = context.scene.world + if idblock and context.scene.texture_context == 'WORLD': return idblock idblock = context.light - if idblock: + if idblock and context.scene.texture_context == 'LIGHT': return idblock - if context.particle_system: + if context.particle_system and context.scene.texture_context == 'PARTICLES': idblock = context.particle_system.settings return idblock idblock = context.line_style - if idblock: + if idblock and context.scene.texture_context == 'LINESTYLE': return idblock @@ -688,7 +831,7 @@ class LIGHT_PT_POV_light(PovLampButtonsPanel, Panel): draw = properties_data_light.DATA_PT_light.draw -class LIGHT_MT_POV_presets(bpy.types.Menu): +class LIGHT_MT_POV_presets(Menu): """Use this class to define preset menu for pov lights.""" bl_label = "Lamp Presets" @@ -1114,9 +1257,8 @@ class WORLD_PT_POV_mist(WorldButtonsPanel, Panel): class RENDER_PT_POV_export_settings(RenderButtonsPanel, Panel): """Use this class to define pov ini settingss buttons.""" - - bl_label = "Start Options" bl_options = {'DEFAULT_CLOSED'} + bl_label = "Auto Start" COMPAT_ENGINES = {'POVRAY_RENDER'} def draw_header(self, context): @@ -1131,6 +1273,7 @@ class RENDER_PT_POV_export_settings(RenderButtonsPanel, Panel): ) def draw(self, context): + layout = self.layout scene = context.scene @@ -1143,25 +1286,25 @@ class RENDER_PT_POV_export_settings(RenderButtonsPanel, Panel): col.prop(scene.pov, "command_line_switches", text="") split = layout.split() - layout.active = not scene.pov.tempfiles_enable - # if not scene.pov.tempfiles_enable: - split.prop(scene.pov, "deletefiles_enable", text="Delete files") - split.prop(scene.pov, "pov_editor", text="POV Editor") + #layout.active = not scene.pov.tempfiles_enable + if not scene.pov.tempfiles_enable: + split.prop(scene.pov, "deletefiles_enable", text="Delete files") + split.prop(scene.pov, "pov_editor", text="POV Editor") - col = layout.column() - col.prop(scene.pov, "scene_name", text="Name") - col.prop(scene.pov, "scene_path", text="Path to files") - # col.prop(scene.pov, "scene_path", text="Path to POV-file") - # col.prop(scene.pov, "renderimage_path", text="Path to image") + col = layout.column() + col.prop(scene.pov, "scene_name", text="Name") + col.prop(scene.pov, "scene_path", text="Path to files") + # col.prop(scene.pov, "scene_path", text="Path to POV-file") + # col.prop(scene.pov, "renderimage_path", text="Path to image") - split = layout.split() - split.prop(scene.pov, "indentation_character", text="Indent") - if scene.pov.indentation_character == 'SPACE': - split.prop(scene.pov, "indentation_spaces", text="Spaces") + split = layout.split() + split.prop(scene.pov, "indentation_character", text="Indent") + if scene.pov.indentation_character == 'SPACE': + split.prop(scene.pov, "indentation_spaces", text="Spaces") - row = layout.row() - row.prop(scene.pov, "comments_enable", text="Comments") - row.prop(scene.pov, "list_lf_enable", text="Line breaks in lists") + row = layout.row() + row.prop(scene.pov, "comments_enable", text="Comments") + row.prop(scene.pov, "list_lf_enable", text="Line breaks in lists") class RENDER_PT_POV_render_settings(RenderButtonsPanel, Panel): @@ -1414,7 +1557,7 @@ class RENDER_PT_POV_radiosity(RenderButtonsPanel, Panel): col.prop(scene.pov, "radio_subsurface") -class POV_RADIOSITY_MT_presets(bpy.types.Menu): +class POV_RADIOSITY_MT_presets(Menu): """Use this class to define pov radiosity presets menu.""" bl_label = "Radiosity Presets" @@ -1562,7 +1705,7 @@ class MODIFIERS_PT_POV_modifiers(ModifierButtonsPanel, Panel): col.prop(ob.pov, "inside_vector") -class MATERIAL_MT_POV_sss_presets(bpy.types.Menu): +class MATERIAL_MT_POV_sss_presets(Menu): """Use this class to define pov sss preset menu.""" bl_label = "SSS Presets" @@ -1872,7 +2015,7 @@ class MATERIAL_PT_POV_mirror(MaterialButtonsPanel, Panel): sub = col.column() sub.active = raym.gloss_factor < 1.0 sub.prop(raym, "gloss_threshold", text="Threshold") - sub.prop(raym, "gloss_samples", text="Samples") + sub.prop(raym, "gloss_samples", text="Noise") sub.prop(raym, "gloss_anisotropic", text="Anisotropic") @@ -2178,7 +2321,7 @@ class MATERIAL_PT_POV_replacement_text(MaterialButtonsPanel, Panel): col.prop(mat.pov, "replacement_text", text="") -class TEXTURE_MT_POV_specials(bpy.types.Menu): +class TEXTURE_MT_POV_specials(Menu): """Use this class to define pov texture slot operations buttons.""" bl_label = "Texture Specials" @@ -2191,14 +2334,20 @@ class TEXTURE_MT_POV_specials(bpy.types.Menu): layout.operator("texture.slot_paste", icon='PASTEDOWN') -class TEXTURE_UL_POV_texture_slots(bpy.types.UIList): - """Use this class to show pov texture slots list.""" # used? - - COMPAT_ENGINES = {'POVRAY_RENDER'} +class WORLD_TEXTURE_SLOTS_UL_POV_layerlist(UIList): + """Use this class to show pov texture slots list.""" # XXX Not used yet + index: bpy.props.IntProperty(name='index') def draw_item( self, context, layout, data, item, icon, active_data, active_propname ): + world = context.scene.world # .pov + active_data = world.pov + # tex = context.texture #may be needed later? + + # We could write some code to decide which icon to use here... + custom_icon = 'TEXTURE' + ob = data slot = item # ma = slot.name @@ -2220,62 +2369,7 @@ class TEXTURE_UL_POV_texture_slots(bpy.types.UIList): layout.label(text="", icon_value=icon) -''' -class MATERIAL_TEXTURE_SLOTS_UL_List(UIList): - """Texture Slots UIList.""" - - - def draw_item(self, context, layout, material, item, icon, active_data, - material_texture_list_index, index): - material = context.material#.pov - active_data = material - #tex = context.texture #may be needed later? - - - # We could write some code to decide which icon to use here... - custom_icon = 'TEXTURE' - - # Make sure your code supports all 3 layout types - if self.layout_type in {'DEFAULT', 'COMPACT'}: - layout.label(item.name, icon = custom_icon) - - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label("", icon = custom_icon) -''' - - -class WORLD_TEXTURE_SLOTS_UL_List(UIList): - """Use this class to show pov texture slots list.""" # XXX Not used yet - - def draw_item( - self, - context, - layout, - world, - item, - icon, - active_data, - active_texture_index, - index, - ): - world = context.world # .pov - active_data = world.pov - # tex = context.texture #may be needed later? - - # We could write some code to decide which icon to use here... - custom_icon = 'TEXTURE' - - # Make sure your code supports all 3 layout types - if self.layout_type in {'DEFAULT', 'COMPACT'}: - layout.label(item.name, icon=custom_icon) - - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label("", icon=custom_icon) - - -class MATERIAL_TEXTURE_SLOTS_UL_POV_layerlist(bpy.types.UIList): +class MATERIAL_TEXTURE_SLOTS_UL_POV_layerlist(UIList): """Use this class to show pov texture slots list.""" # texture_slots: @@ -2304,6 +2398,53 @@ class MATERIAL_TEXTURE_SLOTS_UL_POV_layerlist(bpy.types.UIList): layout.alignment = 'CENTER' layout.label(text="", icon_value=icon) +# Rewrite an existing class to modify. +# register but not unregistered because +# the modified parts concern only POVRAY_RENDER +class TEXTURE_PT_context(TextureButtonsPanel, Panel): + bl_label = "" + bl_context = "texture" + bl_options = {'HIDE_HEADER'} + COMPAT_ENGINES = {'POVRAY_RENDER', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'} + + @classmethod + def poll(cls, context): + return ( + (context.scene.texture_context + not in('MATERIAL','WORLD','LIGHT','PARTICLES','LINESTYLE') + or context.scene.render.engine != 'POVRAY_RENDER') + ) + def draw(self, context): + layout = self.layout + tex = context.texture + space = context.space_data + pin_id = space.pin_id + use_pin_id = space.use_pin_id + user = context.texture_user + + col = layout.column() + + if not (use_pin_id and isinstance(pin_id, bpy.types.Texture)): + pin_id = None + + if not pin_id: + col.template_texture_user() + + if user or pin_id: + col.separator() + + if pin_id: + col.template_ID(space, "pin_id") + else: + propname = context.texture_user_property.identifier + col.template_ID(user, propname, new="texture.new") + + if tex: + col.separator() + + split = col.split(factor=0.2) + split.label(text="Type") + split.prop(tex, "type", text="") class TEXTURE_PT_POV_context_texture(TextureButtonsPanel, Panel): """Use this class to show pov texture context buttons.""" @@ -2316,11 +2457,11 @@ class TEXTURE_PT_POV_context_texture(TextureButtonsPanel, Panel): def poll(cls, context): engine = context.scene.render.engine return engine in cls.COMPAT_ENGINES - # if not (hasattr(context, "texture_slot") or hasattr(context, "texture_node")): + # if not (hasattr(context, "pov_texture_slot") or hasattr(context, "texture_node")): # return False return ( context.material - or context.world + or context.scene.world or context.light or context.texture or context.line_style @@ -2333,11 +2474,12 @@ class TEXTURE_PT_POV_context_texture(TextureButtonsPanel, Panel): layout = self.layout scene = context.scene + mat = context.view_layer.objects.active.active_material + wld = context.scene.world + layout.prop(scene, "texture_context", expand=True) - if scene.texture_context == 'MATERIAL': - mat = context.scene.view_layers[ - "View Layer" - ].objects.active.active_material + if scene.texture_context == 'MATERIAL' and mat is not None: + row = layout.row() row.template_list( "MATERIAL_TEXTURE_SLOTS_UL_POV_layerlist", @@ -2353,22 +2495,90 @@ class TEXTURE_PT_POV_context_texture(TextureButtonsPanel, Panel): col = row.column(align=True) col.operator("pov.textureslotadd", icon='ADD', text='') col.operator("pov.textureslotremove", icon='REMOVE', text='') + #todo: recreate for pov_texture_slots? + #col.operator("texture.slot_move", text="", icon='TRIA_UP').type = 'UP' + #col.operator("texture.slot_move", text="", icon='TRIA_DOWN').type = 'DOWN' col.separator() if mat.pov_texture_slots: index = mat.pov.active_texture_index slot = mat.pov_texture_slots[index] - povtex = slot.name + povtex = slot.texture#slot.name tex = bpy.data.textures[povtex] col.prop(tex, 'use_fake_user', text='') - layout.label(text='Find texture:') + #layout.label(text='Linked Texture data browser:') + propname = slot.texture_search + # if slot.texture was a pointer to texture data rather than just a name string: + # layout.template_ID(povtex, "texture", new="texture.new") + layout.prop_search( - slot, 'texture_search', bpy.data, 'textures', text='' + slot, 'texture_search', bpy.data, 'textures', text='', icon='TEXTURE' ) + try: + bpy.context.tool_settings.image_paint.brush.texture = bpy.data.textures[slot.texture_search] + bpy.context.tool_settings.image_paint.brush.mask_texture = bpy.data.textures[slot.texture_search] + except KeyError: + # texture not hand-linked by user + pass + + if tex: + layout.separator() + split = layout.split(factor=0.2) + split.label(text="Type") + split.prop(tex, "type", text="") + # else: # for i in range(18): # length of material texture slots # mat.pov_texture_slots.add() + elif scene.texture_context == 'WORLD' and wld is not None: + row = layout.row() + row.template_list( + "WORLD_TEXTURE_SLOTS_UL_POV_layerlist", + "", + wld, + "pov_texture_slots", + wld.pov, + "active_texture_index", + rows=2, + maxrows=16, + type="DEFAULT" + ) + col = row.column(align=True) + col.operator("pov.textureslotadd", icon='ADD', text='') + col.operator("pov.textureslotremove", icon='REMOVE', text='') + + #todo: recreate for pov_texture_slots? + #col.operator("texture.slot_move", text="", icon='TRIA_UP').type = 'UP' + #col.operator("texture.slot_move", text="", icon='TRIA_DOWN').type = 'DOWN' + col.separator() + + if wld.pov_texture_slots: + index = wld.pov.active_texture_index + slot = wld.pov_texture_slots[index] + povtex = slot.texture#slot.name + tex = bpy.data.textures[povtex] + col.prop(tex, 'use_fake_user', text='') + #layout.label(text='Linked Texture data browser:') + propname = slot.texture_search + # if slot.texture was a pointer to texture data rather than just a name string: + # layout.template_ID(povtex, "texture", new="texture.new") + + layout.prop_search( + slot, 'texture_search', bpy.data, 'textures', text='', icon='TEXTURE' + ) + try: + bpy.context.tool_settings.image_paint.brush.texture = bpy.data.textures[slot.texture_search] + bpy.context.tool_settings.image_paint.brush.mask_texture = bpy.data.textures[slot.texture_search] + except KeyError: + # texture not hand-linked by user + pass + + if tex: + layout.separator() + split = layout.split(factor=0.2) + split.label(text="Type") + split.prop(tex, "type", text="") # Commented out below is a reminder of what existed in Blender Internal # attributes need to be recreated @@ -2518,7 +2728,7 @@ class TEXTURE_PT_colors(TextureButtonsPanel, Panel): # Texture Slot Panels # -class MATERIAL_OT_POV_texture_slot_add(Operator): +class TEXTURE_OT_POV_texture_slot_add(Operator): """Use this class for the add texture slot button.""" bl_idname = "pov.textureslotadd" @@ -2528,18 +2738,29 @@ class MATERIAL_OT_POV_texture_slot_add(Operator): COMPAT_ENGINES = {'POVRAY_RENDER'} def execute(self, context): - + idblock = pov_context_tex_datablock(context) tex = bpy.data.textures.new(name='Texture', type='IMAGE') - tex.use_fake_user = True - ob = context.scene.view_layers["View Layer"].objects.active - slot = ob.active_material.pov_texture_slots.add() + #tex.use_fake_user = True + #mat = context.view_layer.objects.active.active_material + slot = idblock.pov_texture_slots.add() slot.name = tex.name slot.texture = tex.name + slot.texture_search = tex.name + # Switch paint brush and paint brush mask + # to this texture so settings remain contextual + bpy.context.tool_settings.image_paint.brush.texture = tex + bpy.context.tool_settings.image_paint.brush.mask_texture = tex + idblock.pov.active_texture_index = (len(idblock.pov_texture_slots)-1) + + #for area in bpy.context.screen.areas: + #if area.type in ['PROPERTIES']: + #area.tag_redraw() + return {'FINISHED'} -class MATERIAL_OT_POV_texture_slot_remove(Operator): +class TEXTURE_OT_POV_texture_slot_remove(Operator): """Use this class for the remove texture slot button.""" bl_idname = "pov.textureslotremove" @@ -2549,14 +2770,23 @@ class MATERIAL_OT_POV_texture_slot_remove(Operator): COMPAT_ENGINES = {'POVRAY_RENDER'} def execute(self, context): - pass - # tex = bpy.data.textures.new() - # tex_slot = context.object.active_material.pov_texture_slots.add() - # tex_slot.name = tex.name + idblock = pov_context_tex_datablock(context) + #mat = context.view_layer.objects.active.active_material + tex_slot = idblock.pov_texture_slots.remove(idblock.pov.active_texture_index) + if idblock.pov.active_texture_index > 0: + idblock.pov.active_texture_index -= 1 + try: + tex = idblock.pov_texture_slots[idblock.pov.active_texture_index].texture + except IndexError: + # No more slots + return {'FINISHED'} + # Switch paint brush to previous texture so settings remain contextual + # if 'tex' in locals(): # Would test is the tex variable is assigned / exists + bpy.context.tool_settings.image_paint.brush.texture = bpy.data.textures[tex] + bpy.context.tool_settings.image_paint.brush.mask_texture = bpy.data.textures[tex] return {'FINISHED'} - class TextureSlotPanel(TextureButtonsPanel): """Use this class to show pov texture slots panel.""" @@ -2586,7 +2816,7 @@ class TEXTURE_PT_POV_type(TextureButtonsPanel, Panel): tex = context.texture split = layout.split(factor=0.2) - split.label(text="POV:") + split.label(text="Pattern") split.prop(tex.pov, "tex_pattern_type", text="") # row = layout.row() @@ -2631,6 +2861,7 @@ class TEXTURE_PT_POV_parameters(TextureButtonsPanel, Panel): """Use this class to define pov texture pattern buttons.""" bl_label = "POV Pattern Options" + bl_options = {'HIDE_HEADER'} COMPAT_ENGINES = {'POVRAY_RENDER'} def draw(self, context): @@ -2911,6 +3142,112 @@ class TEXTURE_PT_POV_parameters(TextureButtonsPanel, Panel): row.prop(tex.pov, "warp_turbulence_z", text="Z") row.prop(tex.pov, "modifier_omega", text="Omega") +class TEXTURE_PT_POV_mapping(TextureSlotPanel, Panel): + """Use this class to define POV texture mapping buttons""" + bl_label = "Mapping" + COMPAT_ENGINES = {'POVRAY_RENDER'} + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + + @classmethod + def poll(cls, context): + idblock = pov_context_tex_datablock(context) + if isinstance(idblock, Brush) and not context.sculpt_object: + return False + + if not getattr(context, "texture_slot", None): + return False + + engine = context.scene.render.engine + return (engine in cls.COMPAT_ENGINES) + + def draw(self, context): + layout = self.layout + + idblock = pov_context_tex_datablock(context) + + #tex = context.texture_slot + tex = mat.pov_texture_slots[ + mat.active_texture_index + ] + if not isinstance(idblock, Brush): + split = layout.split(percentage=0.3) + col = split.column() + col.label(text="Coordinates:") + col = split.column() + col.prop(tex, "texture_coords", text="") + + if tex.texture_coords == 'ORCO': + """ + ob = context.object + if ob and ob.type == 'MESH': + split = layout.split(percentage=0.3) + split.label(text="Mesh:") + split.prop(ob.data, "texco_mesh", text="") + """ + elif tex.texture_coords == 'UV': + split = layout.split(percentage=0.3) + split.label(text="Map:") + ob = context.object + if ob and ob.type == 'MESH': + split.prop_search(tex, "uv_layer", ob.data, "uv_textures", text="") + else: + split.prop(tex, "uv_layer", text="") + + elif tex.texture_coords == 'OBJECT': + split = layout.split(percentage=0.3) + split.label(text="Object:") + split.prop(tex, "object", text="") + + elif tex.texture_coords == 'ALONG_STROKE': + split = layout.split(percentage=0.3) + split.label(text="Use Tips:") + split.prop(tex, "use_tips", text="") + + if isinstance(idblock, Brush): + if context.sculpt_object or context.image_paint_object: + brush_texture_settings(layout, idblock, context.sculpt_object) + else: + if isinstance(idblock, FreestyleLineStyle): + split = layout.split(percentage=0.3) + split.label(text="Projection:") + split.prop(tex, "mapping", text="") + + split = layout.split(percentage=0.3) + split.separator() + row = split.row() + row.prop(tex, "mapping_x", text="") + row.prop(tex, "mapping_y", text="") + row.prop(tex, "mapping_z", text="") + + elif isinstance(idblock, Material): + split = layout.split(percentage=0.3) + split.label(text="Projection:") + split.prop(tex, "mapping", text="") + + split = layout.split() + + col = split.column() + if tex.texture_coords in {'ORCO', 'UV'}: + col.prop(tex, "use_from_dupli") + if (idblock.type == 'VOLUME' and tex.texture_coords == 'ORCO'): + col.prop(tex, "use_map_to_bounds") + elif tex.texture_coords == 'OBJECT': + col.prop(tex, "use_from_original") + if (idblock.type == 'VOLUME'): + col.prop(tex, "use_map_to_bounds") + else: + col.label() + + col = split.column() + row = col.row() + row.prop(tex, "mapping_x", text="") + row.prop(tex, "mapping_y", text="") + row.prop(tex, "mapping_z", text="") + + row = layout.row() + row.column().prop(tex, "offset") + row.column().prop(tex, "scale") class TEXTURE_PT_POV_influence(TextureSlotPanel, Panel): """Use this class to define pov texture influence buttons.""" @@ -2919,18 +3256,20 @@ class TEXTURE_PT_POV_influence(TextureSlotPanel, Panel): COMPAT_ENGINES = {'POVRAY_RENDER'} bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' - # bl_context = 'texture' + #bl_context = 'texture' @classmethod def poll(cls, context): idblock = pov_context_tex_datablock(context) if ( - isinstance(idblock, Brush) - and bpy.context.scene.texture_context == 'OTHER' + # isinstance(idblock, Brush) and # Brush used for everything since 2.8 + context.scene.texture_context == 'OTHER' ): # XXX replace by isinstance(idblock, bpy.types.Brush) and ... return False - # if not getattr(context, "pov_texture_slot", None): - # return False + # Specify below also for pov_world_texture_slots, lights etc. + # to display for various types of slots but only when any + if not getattr(idblock, "pov_texture_slots", None): + return False engine = context.scene.render.engine return engine in cls.COMPAT_ENGINES @@ -2940,14 +3279,13 @@ class TEXTURE_PT_POV_influence(TextureSlotPanel, Panel): layout = self.layout idblock = pov_context_tex_datablock(context) - # tex = context.pov_texture_slot - mat = bpy.context.active_object.active_material - texslot = mat.pov_texture_slots[ - mat.active_texture_index + #mat = bpy.context.active_object.active_material + texslot = idblock.pov_texture_slots[ + idblock.pov.active_texture_index ] # bpy.data.textures[mat.active_texture_index] tex = bpy.data.textures[ - mat.pov_texture_slots[mat.active_texture_index].texture + idblock.pov_texture_slots[idblock.pov.active_texture_index].texture ] def factor_but(layout, toggle, factor, name): @@ -3756,7 +4094,7 @@ class OBJECT_PT_povray_replacement_text(ObjectButtonsPanel, Panel): ############################################################################### -class VIEW_MT_POV_primitives_add(bpy.types.Menu): +class VIEW_MT_POV_primitives_add(Menu): """Define the primitives menu with presets""" bl_idname = "VIEW_MT_POV_primitives_add" @@ -3777,7 +4115,7 @@ class VIEW_MT_POV_primitives_add(bpy.types.Menu): layout.menu(VIEW_MT_POV_import.bl_idname, text="Import", icon="IMPORT") -class VIEW_MT_POV_Basic_Shapes(bpy.types.Menu): +class VIEW_MT_POV_Basic_Shapes(Menu): """Use this class to sort simple primitives menu entries.""" bl_idname = "POVRAY_MT_basic_shape_tools" @@ -3859,7 +4197,7 @@ class VIEW_MT_POV_Basic_Shapes(bpy.types.Menu): ) -class VIEW_MT_POV_import(bpy.types.Menu): +class VIEW_MT_POV_import(Menu): """Use this class for the import menu.""" bl_idname = "POVRAY_MT_import_tools" @@ -3910,7 +4248,7 @@ def menu_func_import(self, context): # return True -class NODE_MT_POV_map_create(bpy.types.Menu): +class NODE_MT_POV_map_create(Menu): """Create maps""" bl_idname = "POVRAY_MT_node_map_create" @@ -4056,7 +4394,7 @@ def validinsert(ext): return ext in {".txt", ".inc", ".pov"} -class TEXT_MT_POV_insert(bpy.types.Menu): +class TEXT_MT_POV_insert(Menu): """Use this class to create a menu launcher in text editor for the TEXT_OT_POV_insert operator .""" bl_label = "Insert" @@ -4116,10 +4454,10 @@ class TEXT_PT_POV_custom_code(TextButtonsPanel, Panel): row = box.row() row.prop(text.pov, "custom_code", expand=True) if text.pov.custom_code in {'3dview'}: - box.operator("render.render", icon='OUTLINER_DATA_POSE') + box.operator("render.render", icon='OUTLINER_DATA_ARMATURE') if text.pov.custom_code in {'text'}: rtext = bpy.context.space_data.text - box.operator("text.run", icon='POSE_DATA') + box.operator("text.run", icon='ARMATURE_DATA') # layout.prop(text.pov, "custom_code") elif text.pov.custom_code in {'both'}: box.operator("render.render", icon='POSE_HLT') @@ -4133,7 +4471,7 @@ class TEXT_PT_POV_custom_code(TextButtonsPanel, Panel): # Text editor templates from header menu -class TEXT_MT_POV_templates(bpy.types.Menu): +class TEXT_MT_POV_templates(Menu): """Use this class to create a menu for the same pov templates scenes as other pov IDEs.""" bl_label = "POV" @@ -4154,12 +4492,111 @@ def menu_func_templates(self, context): # Do not depend on POV being active renderer here... self.layout.menu("TEXT_MT_POV_templates") +############################################################################### +# Freestyle +############################################################################### +#import addon_utils +#addon_utils.paths()[0] +#addon_utils.modules() +#mod.bl_info['name'] == 'Freestyle SVG Exporter': +bpy.utils.script_paths("addons") +#render_freestyle_svg = os.path.join(bpy.utils.script_paths("addons"), "render_freestyle_svg.py") + +render_freestyle_svg = bpy.context.preferences.addons.get('render_freestyle_svg') + #mpath=addon_utils.paths()[0].render_freestyle_svg + #import mpath + #from mpath import render_freestyle_svg #= addon_utils.modules(['Freestyle SVG Exporter']) + #from scripts\\addons import render_freestyle_svg +if check_render_freestyle_svg(): + ''' + snippetsWIP + import myscript + import importlib + + importlib.reload(myscript) + myscript.main() + ''' + for member in dir(render_freestyle_svg): + subclass = getattr(render_freestyle_svg, member) + try: + subclass.COMPAT_ENGINES.add('POVRAY_RENDER') + if subclass.bl_idname == "RENDER_PT_SVGExporterPanel": + subclass.bl_parent_id = "RENDER_PT_POV_filter" + subclass.bl_options = {'HIDE_HEADER'} + #subclass.bl_order = 11 + print(subclass.bl_info) + except: + pass + + #del render_freestyle_svg.RENDER_PT_SVGExporterPanel.bl_parent_id + + +class RENDER_PT_POV_filter(RenderButtonsPanel, Panel): + """Use this class to invoke stuff like Freestyle UI.""" + + bl_label = "Freestyle" + bl_options = {'DEFAULT_CLOSED'} + COMPAT_ENGINES = {'POVRAY_RENDER'} + + @classmethod + def poll(cls, context): + with_freestyle = bpy.app.build_options.freestyle + engine = context.scene.render.engine + return(with_freestyle and engine == 'POVRAY_RENDER') + def draw_header(self, context): + + #scene = context.scene + rd = context.scene.render + layout = self.layout + + if rd.use_freestyle: + layout.prop( + rd, "use_freestyle", text="", icon='LINE_DATA' + ) + + else: + layout.prop( + rd, "use_freestyle", text="", icon='OUTLINER_OB_IMAGE' + ) + + def draw(self, context): + rd = context.scene.render + layout = self.layout + layout.active = rd.use_freestyle + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + flow = layout.grid_flow( + row_major=True, + columns=0, + even_columns=True, + even_rows=False, + align=True, + ) + + flow.prop(rd, "line_thickness_mode", expand=True) + + if rd.line_thickness_mode == 'ABSOLUTE': + flow.prop(rd, "line_thickness") + + # Warning if the Freestyle SVG Exporter addon is not enabled + if not check_render_freestyle_svg(): + # col = box.column() + layout.label( + text="Please enable Freestyle SVG Exporter addon", icon="INFO" + ) + # layout.separator() + layout.operator( + "preferences.addon_show", + text="Go to Render: Freestyle SVG Exporter addon", + icon="PREFERENCES", + ).module = "render_freestyle_svg" classes = ( WORLD_PT_POV_world, WORLD_MT_POV_presets, WORLD_OT_POV_add_preset, - WORLD_TEXTURE_SLOTS_UL_List, + WORLD_TEXTURE_SLOTS_UL_POV_layerlist, + #WORLD_TEXTURE_SLOTS_UL_List, WORLD_PT_POV_mist, # RenderButtonsPanel, # ModifierButtonsPanel, @@ -4188,6 +4625,7 @@ classes = ( RENDER_PT_POV_photons, RENDER_PT_POV_antialias, RENDER_PT_POV_radiosity, + RENDER_PT_POV_filter, POV_RADIOSITY_MT_presets, RENDER_OT_POV_radiosity_add_preset, RENDER_PT_POV_media, @@ -4232,13 +4670,14 @@ classes = ( TEXT_MT_POV_insert, TEXT_PT_POV_custom_code, TEXT_MT_POV_templates, - # TEXTURE_PT_context, - # TEXTURE_PT_POV_povray_texture_slots, - TEXTURE_UL_POV_texture_slots, + TEXTURE_PT_context, + #TEXTURE_PT_POV_povray_texture_slots, + #TEXTURE_UL_POV_texture_slots, MATERIAL_TEXTURE_SLOTS_UL_POV_layerlist, - MATERIAL_OT_POV_texture_slot_add, - MATERIAL_OT_POV_texture_slot_remove, + TEXTURE_OT_POV_texture_slot_add, + TEXTURE_OT_POV_texture_slot_remove, TEXTURE_PT_POV_influence, + TEXTURE_PT_POV_mapping, ) @@ -4257,11 +4696,18 @@ def register(): # was used for parametric objects but made the other addon unreachable on # unregister for other tools to use created a user action call instead # addon_utils.enable("add_mesh_extra_objects", default_set=False, persistent=True) - # bpy.types.TEXTURE_PT_context_texture.prepend(TEXTURE_PT_POV_type) + if not povCentricWorkspace in bpy.app.handlers.load_post: + print("Adding POV wentric workspace on load handlers list") + bpy.app.handlers.load_post.append(povCentricWorkspace) def unregister(): + if povCentricWorkspace in bpy.app.handlers.load_post: + print("Removing POV wentric workspace from load handlers list") + bpy.app.handlers.load_post.remove(povCentricWorkspace) + + # from bpy.utils import unregister_class # bpy.types.TEXTURE_PT_context_texture.remove(TEXTURE_PT_POV_type) @@ -4274,4 +4720,5 @@ def unregister(): bpy.types.VIEW3D_MT_add.remove(menu_func_add) for cls in reversed(classes): - unregister_class(cls) + if cls != TEXTURE_PT_context: + unregister_class(cls) -- cgit v1.2.3 From adac42a463344b288882954e57fca0715ee398f3 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Sat, 1 Aug 2020 23:16:39 -0400 Subject: Collection Manager: Fix T78985. Task: T69577 Refactored the functions get_move_selection and get_move_active to be faster by using sets and looping through all the objects instead of looping through the selected objects and using direct object lookups, except for special cases where direct lookups are actually faster. Removed unneeded calls to get_move_selection and get_move_active. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/internals.py | 16 ++++++++++++---- object_collection_manager/qcd_move_widget.py | 6 ++++-- object_collection_manager/ui.py | 23 +++++++++++++++-------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index ad67c29b..1e74a3c0 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 2), + "version": (2, 12, 3), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py index 8a225443..857e73aa 100644 --- a/object_collection_manager/internals.py +++ b/object_collection_manager/internals.py @@ -597,13 +597,21 @@ def generate_state(): return state -def get_move_selection(): +def get_move_selection(*, names_only=False): global move_selection if not move_selection: - move_selection = [obj.name for obj in bpy.context.selected_objects] + move_selection = {obj.name for obj in bpy.context.selected_objects} - return [bpy.data.objects[name] for name in move_selection] + if names_only: + return move_selection + + else: + if len(move_selection) <= 5: + return {bpy.data.objects[name] for name in move_selection} + + else: + return {obj for obj in bpy.data.objects if obj.name in move_selection} def get_move_active(): @@ -613,7 +621,7 @@ def get_move_active(): if not move_active: move_active = getattr(bpy.context.view_layer.objects.active, "name", None) - if move_active not in [obj.name for obj in get_move_selection()]: + if move_active not in get_move_selection(names_only=True): move_active = None return bpy.data.objects[move_active] if move_active else None diff --git a/object_collection_manager/qcd_move_widget.py b/object_collection_manager/qcd_move_widget.py index 28ed9c01..1b2a6bee 100644 --- a/object_collection_manager/qcd_move_widget.py +++ b/object_collection_manager/qcd_move_widget.py @@ -655,6 +655,10 @@ def allocate_main_ui(self, context): self.areas["Button Row 2 B"] = button_row_2_b + selected_objects = qcd_operators.get_move_selection() + active_object = qcd_operators.get_move_active() + + # BUTTONS def get_buttons(button_row, row_num): cur_width_pos = button_row["vert"][0] @@ -667,8 +671,6 @@ def allocate_main_ui(self, context): if qcd_slot_name: qcd_laycol = layer_collections[qcd_slot_name]["ptr"] collection_objects = qcd_laycol.collection.objects - selected_objects = qcd_operators.get_move_selection() - active_object = qcd_operators.get_move_active() # BUTTON x = cur_width_pos diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index c6403fe2..7858e5bf 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -178,8 +178,12 @@ class CollectionManager(Operator): row_setcol = global_rto_row.row() row_setcol.alignment = 'LEFT' row_setcol.operator_context = 'INVOKE_DEFAULT' + selected_objects = get_move_selection() active_object = get_move_active() + CM_UL_items.selected_objects = selected_objects + CM_UL_items.active_object = active_object + collection = context.view_layer.layer_collection.collection icon = 'MESH_CUBE' @@ -188,7 +192,7 @@ class CollectionManager(Operator): if active_object and active_object.name in collection.objects: icon = 'SNAP_VOLUME' - elif not set(selected_objects).isdisjoint(collection.objects): + elif not selected_objects.isdisjoint(collection.objects): icon = 'STICKY_UVS_LOC' else: @@ -437,6 +441,9 @@ class CollectionManager(Operator): class CM_UL_items(UIList): last_filter_value = "" + selected_objects = set() + active_object = None + filter_by_selected: BoolProperty( name="Filter By Selected", default=False, @@ -456,8 +463,8 @@ class CM_UL_items(UIList): view_layer = context.view_layer laycol = layer_collections[item.name] collection = laycol["ptr"].collection - selected_objects = get_move_selection() - active_object = get_move_active() + selected_objects = CM_UL_items.selected_objects + active_object = CM_UL_items.active_object column = layout.column(align=True) @@ -545,7 +552,7 @@ class CM_UL_items(UIList): if active_object and active_object.name in collection.objects: icon = 'SNAP_VOLUME' - elif not set(selected_objects).isdisjoint(collection.objects): + elif not selected_objects.isdisjoint(collection.objects): icon = 'STICKY_UVS_LOC' else: @@ -781,14 +788,15 @@ def view3d_header_qcd_slots(self, context): update_collection_tree(context) + selected_objects = get_move_selection() + active_object = get_move_active() + for x in range(20): qcd_slot_name = qcd_slots.get_name(str(x+1)) if qcd_slot_name: qcd_laycol = layer_collections[qcd_slot_name]["ptr"] collection_objects = qcd_laycol.collection.objects - selected_objects = get_move_selection() - active_object = get_move_active() icon_value = 0 @@ -797,9 +805,8 @@ def view3d_header_qcd_slots(self, context): active_object.name in collection_objects): icon = 'LAYER_ACTIVE' - # if there are selected objects use LAYER_ACTIVE - elif not set(selected_objects).isdisjoint(collection_objects): + elif not selected_objects.isdisjoint(collection_objects): icon = 'LAYER_USED' # If there are objects use LAYER_USED -- cgit v1.2.3 From 177ca9aeb9e1b00834b9dc6dea54c4ab792df6bf Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Sat, 1 Aug 2020 23:16:39 -0400 Subject: Collection Manager: Fix T78985. Task: T69577 Refactored the functions get_move_selection and get_move_active to be faster by using sets and looping through all the objects instead of looping through the selected objects and using direct object lookups, except for special cases where direct lookups are actually faster. Removed unneeded calls to get_move_selection and get_move_active. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/internals.py | 16 ++++++++++++---- object_collection_manager/qcd_move_widget.py | 6 ++++-- object_collection_manager/ui.py | 23 +++++++++++++++-------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index ad67c29b..1e74a3c0 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 2), + "version": (2, 12, 3), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py index 8a225443..857e73aa 100644 --- a/object_collection_manager/internals.py +++ b/object_collection_manager/internals.py @@ -597,13 +597,21 @@ def generate_state(): return state -def get_move_selection(): +def get_move_selection(*, names_only=False): global move_selection if not move_selection: - move_selection = [obj.name for obj in bpy.context.selected_objects] + move_selection = {obj.name for obj in bpy.context.selected_objects} - return [bpy.data.objects[name] for name in move_selection] + if names_only: + return move_selection + + else: + if len(move_selection) <= 5: + return {bpy.data.objects[name] for name in move_selection} + + else: + return {obj for obj in bpy.data.objects if obj.name in move_selection} def get_move_active(): @@ -613,7 +621,7 @@ def get_move_active(): if not move_active: move_active = getattr(bpy.context.view_layer.objects.active, "name", None) - if move_active not in [obj.name for obj in get_move_selection()]: + if move_active not in get_move_selection(names_only=True): move_active = None return bpy.data.objects[move_active] if move_active else None diff --git a/object_collection_manager/qcd_move_widget.py b/object_collection_manager/qcd_move_widget.py index 28ed9c01..1b2a6bee 100644 --- a/object_collection_manager/qcd_move_widget.py +++ b/object_collection_manager/qcd_move_widget.py @@ -655,6 +655,10 @@ def allocate_main_ui(self, context): self.areas["Button Row 2 B"] = button_row_2_b + selected_objects = qcd_operators.get_move_selection() + active_object = qcd_operators.get_move_active() + + # BUTTONS def get_buttons(button_row, row_num): cur_width_pos = button_row["vert"][0] @@ -667,8 +671,6 @@ def allocate_main_ui(self, context): if qcd_slot_name: qcd_laycol = layer_collections[qcd_slot_name]["ptr"] collection_objects = qcd_laycol.collection.objects - selected_objects = qcd_operators.get_move_selection() - active_object = qcd_operators.get_move_active() # BUTTON x = cur_width_pos diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index c6403fe2..7858e5bf 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -178,8 +178,12 @@ class CollectionManager(Operator): row_setcol = global_rto_row.row() row_setcol.alignment = 'LEFT' row_setcol.operator_context = 'INVOKE_DEFAULT' + selected_objects = get_move_selection() active_object = get_move_active() + CM_UL_items.selected_objects = selected_objects + CM_UL_items.active_object = active_object + collection = context.view_layer.layer_collection.collection icon = 'MESH_CUBE' @@ -188,7 +192,7 @@ class CollectionManager(Operator): if active_object and active_object.name in collection.objects: icon = 'SNAP_VOLUME' - elif not set(selected_objects).isdisjoint(collection.objects): + elif not selected_objects.isdisjoint(collection.objects): icon = 'STICKY_UVS_LOC' else: @@ -437,6 +441,9 @@ class CollectionManager(Operator): class CM_UL_items(UIList): last_filter_value = "" + selected_objects = set() + active_object = None + filter_by_selected: BoolProperty( name="Filter By Selected", default=False, @@ -456,8 +463,8 @@ class CM_UL_items(UIList): view_layer = context.view_layer laycol = layer_collections[item.name] collection = laycol["ptr"].collection - selected_objects = get_move_selection() - active_object = get_move_active() + selected_objects = CM_UL_items.selected_objects + active_object = CM_UL_items.active_object column = layout.column(align=True) @@ -545,7 +552,7 @@ class CM_UL_items(UIList): if active_object and active_object.name in collection.objects: icon = 'SNAP_VOLUME' - elif not set(selected_objects).isdisjoint(collection.objects): + elif not selected_objects.isdisjoint(collection.objects): icon = 'STICKY_UVS_LOC' else: @@ -781,14 +788,15 @@ def view3d_header_qcd_slots(self, context): update_collection_tree(context) + selected_objects = get_move_selection() + active_object = get_move_active() + for x in range(20): qcd_slot_name = qcd_slots.get_name(str(x+1)) if qcd_slot_name: qcd_laycol = layer_collections[qcd_slot_name]["ptr"] collection_objects = qcd_laycol.collection.objects - selected_objects = get_move_selection() - active_object = get_move_active() icon_value = 0 @@ -797,9 +805,8 @@ def view3d_header_qcd_slots(self, context): active_object.name in collection_objects): icon = 'LAYER_ACTIVE' - # if there are selected objects use LAYER_ACTIVE - elif not set(selected_objects).isdisjoint(collection_objects): + elif not selected_objects.isdisjoint(collection_objects): icon = 'LAYER_USED' # If there are objects use LAYER_USED -- cgit v1.2.3 From cfd0a58e26e5bd3a49df3b01c95dfd65a3865327 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Sun, 2 Aug 2020 00:33:26 -0400 Subject: Collection Manager: Fix tooltip. Task: T69577 Fix Apply Phantom Mode tooltip to be clearer. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/operators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 1e74a3c0..4fe792bb 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 3), + "version": (2, 12, 4), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 177ab3d7..08c90224 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -1100,7 +1100,7 @@ class CMPhantomModeOperator(Operator): class CMApplyPhantomModeOperator(Operator): - '''Make all changes made in Phantom Mode permanent''' + '''Apply changes and quit Phantom Mode''' bl_label = "Apply Phantom Mode" bl_idname = "view3d.apply_phantom_mode" -- cgit v1.2.3 From 9970c2403400831b0c8af6f9b2244aa4eee1cf8b Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Sun, 2 Aug 2020 00:33:26 -0400 Subject: Collection Manager: Fix tooltip. Task: T69577 Fix Apply Phantom Mode tooltip to be clearer. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/operators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 1e74a3c0..4fe792bb 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 3), + "version": (2, 12, 4), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 177ab3d7..08c90224 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -1100,7 +1100,7 @@ class CMPhantomModeOperator(Operator): class CMApplyPhantomModeOperator(Operator): - '''Make all changes made in Phantom Mode permanent''' + '''Apply changes and quit Phantom Mode''' bl_label = "Apply Phantom Mode" bl_idname = "view3d.apply_phantom_mode" -- cgit v1.2.3 From b40b8b4527d60f0bb37327bf5abaeb659794fa81 Mon Sep 17 00:00:00 2001 From: Dalai Felinto Date: Mon, 3 Aug 2020 09:35:18 +0200 Subject: POVRAY: Fix unittest Broken on rBAe44e5845ee2b --- render_povray/ui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/render_povray/ui.py b/render_povray/ui.py index 9ac79067..1daec148 100644 --- a/render_povray/ui.py +++ b/render_povray/ui.py @@ -4670,7 +4670,6 @@ classes = ( TEXT_MT_POV_insert, TEXT_PT_POV_custom_code, TEXT_MT_POV_templates, - TEXTURE_PT_context, #TEXTURE_PT_POV_povray_texture_slots, #TEXTURE_UL_POV_texture_slots, MATERIAL_TEXTURE_SLOTS_UL_POV_layerlist, -- cgit v1.2.3 From bbe0cee094a48761118f8efe3f9b84ae3f110a2c Mon Sep 17 00:00:00 2001 From: Maurice Raybaud Date: Mon, 3 Aug 2020 23:08:35 +0200 Subject: POV: Improve specific pov workspace *fix: too invasive default workspace, now only triggers if pov was saved to default renderer during previous session *add: opening text editor sidebar to better show pov specific tools and text editing nature of the area. --- render_povray/ui.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/render_povray/ui.py b/render_povray/ui.py index 1daec148..6bdadccf 100644 --- a/render_povray/ui.py +++ b/render_povray/ui.py @@ -259,13 +259,10 @@ for member in dir( del properties_particle - -#Use some delay? -#https://blenderartists.org/t/restrictdata-object-has-no-attribute-scenes-how-to-avoid-it/579885/4 ############# POV-Centric WORSPACE ############# @persistent def povCentricWorkspace(dummy): - """Set up a POV centric Workspace if addon was left activate from previous session + """Set up a POV centric Workspace if addon was activated and saved as default renderer This would bring a ’_RestrictData’ error because UI needs to be fully loaded before workspace changes so registering this function in bpy.app.handlers is needed. @@ -274,10 +271,9 @@ def povCentricWorkspace(dummy): bpy.app.handlers.persistent decorator is used (@persistent) above. """ - #bpy.context.scene.render.engine = 'POVRAY_RENDER' wsp = bpy.data.workspaces.get('Scripting') context = bpy.context - if wsp is not None: + if wsp is not None and context.scene.render.engine == 'POVRAY_RENDER': new_wsp = bpy.ops.workspace.duplicate({'workspace': wsp}) bpy.data.workspaces['Scripting.001'].name='POV' # Already done it would seem, but explicitly make this workspaces the active one @@ -302,6 +298,7 @@ def povCentricWorkspace(dummy): #bpy.ops.screen.area_move(override, x=(area.x + 5), y=area.y, delta=-100) bpy.ops.screen.space_type_set_or_cycle(override, space_type = 'TEXT_EDITOR') + space.show_region_ui = True #bpy.ops.screen.region_scale(override) #bpy.ops.screen.region_scale() break -- cgit v1.2.3 From 9adfdc230051f15db24a8bc32c4828e57353513f Mon Sep 17 00:00:00 2001 From: Bastien Montagne Date: Tue, 4 Aug 2020 13:27:17 +0200 Subject: Cleanup: Typos & co in UI messages. --- viewport_vr_preview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewport_vr_preview.py b/viewport_vr_preview.py index c7d1d1af..44e8897b 100644 --- a/viewport_vr_preview.py +++ b/viewport_vr_preview.py @@ -181,7 +181,7 @@ class VRLandmark(PropertyGroup): "Use an existing camera to define the VR view base location and " "rotation"), ('CUSTOM', "Custom Pose", - "Allow a manually definied position and rotation to be used as " + "Allow a manually defined position and rotation to be used as " "the VR view base pose"), ], default='SCENE_CAMERA', -- cgit v1.2.3 From e74f7ad92d166cd087e3cab1e2ff14936215d036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Sat, 1 Aug 2020 20:15:33 +0200 Subject: BlenderKit: fix appending of assets This would unnecessarily create full copies instead of linked objects (as linked data, not linked from outer file) --- blenderkit/download.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/blenderkit/download.py b/blenderkit/download.py index a93611c0..545b06f2 100644 --- a/blenderkit/download.py +++ b/blenderkit/download.py @@ -316,6 +316,18 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, al = 'LINK' else: al = 'APPEND' + if asset_data['assetType'] == 'model': + source_parent = get_asset_in_scene(asset_data) + parent, new_obs = duplicate_asset(source=source_parent, **kwargs) + parent.location = kwargs['model_location'] + parent.rotation_euler = kwargs['model_rotation'] + # this is a case where asset is already in scene and should be duplicated instead. + # there is a big chance that the duplication wouldn't work perfectly(hidden or unselectable objects) + # so here we need to check and return if there was success + # also, if it was successful, no other operations are needed , basically all asset data is already ready from the original asset + if new_obs: + bpy.ops.wm.undo_push_context(message='add %s to scene' % asset_data['name']) + return # first get conditions for append link link = al == 'LINK' @@ -333,7 +345,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, return if link: - parent, newobs = append_link.link_collection(file_names[-1], + parent, new_obs = append_link.link_collection(file_names[-1], location=downloader['location'], rotation=downloader['rotation'], link=link, @@ -342,7 +354,7 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, else: - parent, newobs = append_link.append_objects(file_names[-1], + parent, new_obs = append_link.append_objects(file_names[-1], location=downloader['location'], rotation=downloader['rotation'], link=link, @@ -356,21 +368,21 @@ def append_asset(asset_data, **kwargs): # downloaders=[], location=None, elif kwargs.get('model_location') is not None: if link: - parent, newobs = append_link.link_collection(file_names[-1], + parent, new_obs = append_link.link_collection(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], link=link, name=asset_data['name'], parent=kwargs.get('parent')) else: - parent, newobs = append_link.append_objects(file_names[-1], + parent, new_obs = append_link.append_objects(file_names[-1], location=kwargs['model_location'], rotation=kwargs['model_rotation'], link=link, name=asset_data['name'], parent=kwargs.get('parent')) - #scale Empty for assets, so they don't clutter the scene. + # scale Empty for assets, so they don't clutter the scene. if parent.type == 'EMPTY' and link: bmin = asset_data['bbox_min'] bmax = asset_data['bbox_max'] @@ -733,6 +745,66 @@ def try_finished_append(asset_data, **kwargs): # location=None, material_target return done +def get_asset_in_scene(asset_data): + '''tries to find an appended copy of particular asset and duplicate it - so it doesn't have to be appended again.''' + scene = bpy.context.scene + for ob in bpy.context.scene.objects: + ad1 = ob.get('asset_data') + if not ad1: + continue + if ad1.get('assetBaseId') == asset_data['assetBaseId']: + return ob + return None + + +def check_all_visible(obs): + '''checks all objects are visible, so they can be manipulated/copied.''' + for ob in obs: + if not ob.visible_get(): + return False + return True + + +def check_selectible(obs): + '''checks if all objects can be selected and selects them if possible. + this isn't only select_hide, but all possible combinations of collections e.t.c. so hard to check otherwise.''' + for ob in obs: + ob.select_set(True) + if not ob.select_get(): + return False + return True + + +def duplicate_asset(source, **kwargs): + '''Duplicate asset when it's already appended in the scene, so that blender's append doesn't create duplicated data.''' + + # we need to save selection + sel = utils.selection_get() + bpy.ops.object.select_all(action='DESELECT') + + # check visibility + obs = utils.get_hierarchy(source) + if not check_all_visible(obs): + return None + # check selectability and select in one run + if not check_selectible(obs): + return None + + # duplicate the asset objects + bpy.ops.object.duplicate(linked=True) + + + nobs = bpy.context.selected_objects[:] + #get parent + for ob in nobs: + if ob.parent not in nobs: + parent = ob + break + # restore original selection + utils.selection_set(sel) + return parent , nobs + + def asset_in_scene(asset_data): '''checks if the asset is already in scene. If yes, modifies asset data so the asset can be reached again.''' scene = bpy.context.scene @@ -746,10 +818,10 @@ def asset_in_scene(asset_data): asset_data['file_name'] = ad['file_name'] asset_data['url'] = ad['url'] - #browse all collections since linked collections can have same name. + # browse all collections since linked collections can have same name. for c in bpy.data.collections: if c.name == ad['name']: - #there can also be more linked collections with same name, we need to check id. + # there can also be more linked collections with same name, we need to check id. if c.library and c.library.get('asset_data') and c.library['asset_data']['assetBaseId'] == id: return 'LINKED' return 'APPENDED' -- cgit v1.2.3 From 54c325fd995edf501f2d2929f4e7584a173cf457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vil=C3=A9m=20Duha?= Date: Tue, 4 Aug 2020 13:29:31 +0200 Subject: BlenderKit: fix upload -upload was broken since changes in append_link - fake context wasn't possible there,reverting to old append method in such case. -also assets inside assets could cause problem with ratings drawing check --- blenderkit/append_link.py | 74 +++++++++++++++++++++++++---------------------- blenderkit/ratings.py | 3 ++ blenderkit/ui.py | 5 +++- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/blenderkit/append_link.py b/blenderkit/append_link.py index 5af63fe1..56b2857d 100644 --- a/blenderkit/append_link.py +++ b/blenderkit/append_link.py @@ -190,43 +190,45 @@ def append_particle_system(file_name, obnames=[], location=(0, 0, 0), link=False def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs): '''append objects into scene individually''' #simplified version of append - scene = bpy.context.scene - sel = utils.selection_get() - bpy.ops.object.select_all(action='DESELECT') - - path = file_name + "\\Collection\\" - object_name = kwargs.get('name') - fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D') - bpy.ops.wm.append(fc,filename=object_name, directory=path) - - - return_obs = [] - for ob in bpy.context.scene.objects: - if ob.select_get(): - return_obs.append(ob) - if not ob.parent: - main_object = ob - ob.location = location - - if kwargs.get('rotation'): - main_object.rotation_euler = kwargs['rotation'] - - if kwargs.get('parent') is not None: - main_object.parent = bpy.data.objects[kwargs['parent']] - main_object.matrix_world.translation = location - - bpy.ops.object.select_all(action='DESELECT') - utils.selection_set(sel) - - return main_object, return_obs - + if kwargs.get('name'): + # by now used for appending into scene + scene = bpy.context.scene + sel = utils.selection_get() + bpy.ops.object.select_all(action='DESELECT') + + path = file_name + "\\Collection\\" + object_name = kwargs.get('name') + fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D') + bpy.ops.wm.append(fc, filename=object_name, directory=path) + + + return_obs = [] + for ob in bpy.context.scene.objects: + if ob.select_get(): + return_obs.append(ob) + if not ob.parent: + main_object = ob + ob.location = location + + if kwargs.get('rotation'): + main_object.rotation_euler = kwargs['rotation'] + + if kwargs.get('parent') is not None: + main_object.parent = bpy.data.objects[kwargs['parent']] + main_object.matrix_world.translation = location + + bpy.ops.object.select_all(action='DESELECT') + utils.selection_set(sel) + + return main_object, return_obs + #this is used for uploads: with bpy.data.libraries.load(file_name, link=link, relative=True) as (data_from, data_to): sobs = [] - for col in data_from.collections: - if col == kwargs.get('name'): - for ob in col.objects: - if ob in obnames or obnames == []: - sobs.append(ob) + # for col in data_from.collections: + # if col == kwargs.get('name'): + for ob in data_from.objects: + if ob in obnames or obnames == []: + sobs.append(ob) data_to.objects = sobs # data_to.objects = data_from.objects#[name for name in data_from.objects if name.startswith("house")] @@ -260,6 +262,8 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar for ob in hidden_objects: ob.hide_viewport = True + print(return_obs) + print(main_object) if kwargs.get('rotation') is not None: main_object.rotation_euler = kwargs['rotation'] diff --git a/blenderkit/ratings.py b/blenderkit/ratings.py index 800749c8..7d246a9f 100644 --- a/blenderkit/ratings.py +++ b/blenderkit/ratings.py @@ -349,6 +349,9 @@ class FastRateMenu(Operator): ('20', '20', ''), ('50', '50', ''), ('100', '100', ''), + ('150', '100', ''), + ('200', '100', ''), + ('250', '100', ''), ], default='0', update=update_ratings_work_hours_ui ) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 2822b826..5333b65e 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -1019,9 +1019,12 @@ def is_rating_possible(): # crawl parents to reach active asset. there could have been parenting so we need to find the first onw ao_check = ao while ad is None or (ad is None and ao_check.parent is not None): + s = bpy.context.scene ad = ao_check.get('asset_data') if ad is not None: - rated = bpy.context.scene['assets rated'].get(ad['assetBaseId']) + + s['assets rated'] = s.get('assets rated',{}) + rated = s['assets rated'].get(ad['assetBaseId']) # originally hidden for already rated assets return True, rated, ao_check, ad elif ao_check.parent is not None: -- cgit v1.2.3 From d777821fd6ad33be4c8bdf8dbedb28827f57caa5 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Wed, 5 Aug 2020 11:36:21 +1000 Subject: Fix T79532: Crash on undo after glTF import --- io_scene_gltf2/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 70a06504..d911c97f 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -844,6 +844,7 @@ class ImportGLTF2(Operator, ImportHelper): """Load a glTF 2.0 file""" bl_idname = 'import_scene.gltf' bl_label = 'Import glTF 2.0' + bl_options = {'REGISTER', 'UNDO'} filter_glob: StringProperty(default="*.glb;*.gltf", options={'HIDDEN'}) -- cgit v1.2.3 From 919541ffbeb34af259f2950c785d9cb7cc392f33 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Wed, 5 Aug 2020 11:38:23 +1000 Subject: Cleanup: trailing space --- add_mesh_BoltFactory/Boltfactory.py | 8 +- add_mesh_BoltFactory/createMesh.py | 154 ++++++++++----------- io_coat3D/__init__.py | 58 ++++---- io_coat3D/tex.py | 30 ++-- io_coat3D/texVR.py | 24 ++-- oscurart_tools/__init__.py | 4 +- oscurart_tools/mesh/overlap_uvs.py | 32 ++--- oscurart_tools/mesh/print_uv_stats.py | 28 ++-- oscurart_tools/render/material_overrides.py | 56 ++++---- oscurart_tools/render/render_tokens.py | 10 +- .../operators/scene_create_from_selection.py | 10 +- .../scripts/BPSRender/bpsrender/__init__.py | 1 - 12 files changed, 207 insertions(+), 208 deletions(-) diff --git a/add_mesh_BoltFactory/Boltfactory.py b/add_mesh_BoltFactory/Boltfactory.py index acfe5886..244d9720 100644 --- a/add_mesh_BoltFactory/Boltfactory.py +++ b/add_mesh_BoltFactory/Boltfactory.py @@ -141,7 +141,7 @@ class add_mesh_bolt(Operator, AddObjectHelper): ('bf_Torx_T50', 'T50', 'T50'), ('bf_Torx_T55', 'T55', 'T55'), ] - + bf_Torx_Size_Type: EnumProperty( attr='bf_Torx_Size_Type', name='Torx Size', @@ -323,7 +323,7 @@ class add_mesh_bolt(Operator, AddObjectHelper): description='Height of the 12 Point Nut', unit='LENGTH', ) - + bf_12_Point_Nut_Flat_Distance: FloatProperty( attr='bf_12_Point_Nut_Flat_Distance', name='12 Point Nut Flat Dist', default=3.0, @@ -400,8 +400,8 @@ class add_mesh_bolt(Operator, AddObjectHelper): col.prop(self, 'bf_Hex_Nut_Height') col.prop(self, 'bf_Hex_Nut_Flat_Distance') - - + + # Thread col.label(text='Thread') if self.bf_Model_Type == 'bf_Model_Bolt': diff --git a/add_mesh_BoltFactory/createMesh.py b/add_mesh_BoltFactory/createMesh.py index 96284012..e19f15ba 100644 --- a/add_mesh_BoltFactory/createMesh.py +++ b/add_mesh_BoltFactory/createMesh.py @@ -291,14 +291,14 @@ def Fill_Fan_Face(OFFSET, NUM, FACE_DOWN=0): Ret = [] Face = [NUM-1,0,1] TempFace = [0, 0, 0] - A = 0 + A = 0 #B = 1 unsed C = 2 if NUM < 3: return None for _i in range(NUM - 2): TempFace[0] = Face[A] - TempFace[1] = Face[C] + TempFace[1] = Face[C] TempFace[2] = Face[C]+1 if FACE_DOWN: Ret.append([OFFSET + Face[2], OFFSET + Face[1], OFFSET + Face[0]]) @@ -429,18 +429,18 @@ def Torx_Fill(OFFSET, FLIP=0): Lookup = [[0,10,11], [0,11, 12], [0,12,1], - + [1, 12, 13], [1, 13, 14], [1, 14, 15], [1, 15, 2], - + [2, 15, 16], [2, 16, 17], [2, 17, 18], [2, 18, 19], [2, 19, 3], - + [3, 19, 20], [3, 20, 21], [3, 21, 22], @@ -448,8 +448,8 @@ def Torx_Fill(OFFSET, FLIP=0): [3, 23, 24], [3, 24, 25], [3, 25, 4], - - + + [4, 25, 26], [4, 26, 27], [4, 27, 28], @@ -457,25 +457,25 @@ def Torx_Fill(OFFSET, FLIP=0): [4, 29, 30], [4, 30, 31], [4, 31, 5], - + [5, 31, 32], [5, 32, 33], [5, 33, 34], [5, 34, 35], [5, 35, 36], [5, 36, 6], - + [6, 36, 37], [6, 37, 38], [6, 38, 39], [6, 39, 7], - + [7, 39, 40], [7, 40, 41], [7, 41, 42], [7, 42, 43], [7, 43, 8], - + [8, 43, 44], [8, 44, 45], [8, 45, 46], @@ -505,40 +505,40 @@ def Create_Torx_Bit(Point_Distance, HEIGHT): OUTTER_RADIUS = POINT_RADIUS * 1.05 POINT_1_Y = POINT_RADIUS * 0.816592592592593 - POINT_2_X = POINT_RADIUS * 0.511111111111111 - POINT_2_Y = POINT_RADIUS * 0.885274074074074 - POINT_3_X = POINT_RADIUS * 0.7072 - POINT_3_Y = POINT_RADIUS * 0.408296296296296 - POINT_4_X = POINT_RADIUS * 1.02222222222222 - SMALL_RADIUS = POINT_RADIUS * 0.183407407407407 - BIG_RADIUS = POINT_RADIUS * 0.333333333333333 + POINT_2_X = POINT_RADIUS * 0.511111111111111 + POINT_2_Y = POINT_RADIUS * 0.885274074074074 + POINT_3_X = POINT_RADIUS * 0.7072 + POINT_3_Y = POINT_RADIUS * 0.408296296296296 + POINT_4_X = POINT_RADIUS * 1.02222222222222 + SMALL_RADIUS = POINT_RADIUS * 0.183407407407407 + BIG_RADIUS = POINT_RADIUS * 0.333333333333333 # Values for T40 # POINT_1_Y = 2.756 # POINT_2_X = 1.725 -# POINT_2_Y = 2.9878 +# POINT_2_Y = 2.9878 # POINT_3_X = 2.3868 # POINT_3_Y = 1.378 # POINT_4_X = 3.45 -# +# # SMALL_RADIUS = 0.619 # BIG_RADIUS = 1.125 def Do_Curve(Curve_Height): - for i in range(0, 90, 10): + for i in range(0, 90, 10): x = sin(radians(i)) * SMALL_RADIUS y = cos(radians(i)) * SMALL_RADIUS verts.append([x, POINT_1_Y + y, Curve_Height]) - - for i in range(260, 150, -10): + + for i in range(260, 150, -10): x = sin(radians(i)) * BIG_RADIUS y = cos(radians(i)) * BIG_RADIUS verts.append([POINT_2_X + x, POINT_2_Y + y, Curve_Height]) - + for i in range(340, 150 + 360, 10): x = sin(radians(i%360)) * SMALL_RADIUS y = cos(radians(i%360)) * SMALL_RADIUS verts.append([POINT_3_X + x, POINT_3_Y + y, Curve_Height]) - - for i in range(320, 260, -10): + + for i in range(320, 260, -10): x = sin(radians(i)) * BIG_RADIUS y = cos(radians(i)) * BIG_RADIUS verts.append([POINT_4_X + x, y, Curve_Height]) @@ -553,19 +553,19 @@ def Create_Torx_Bit(Point_Distance, HEIGHT): FaceStart_Top_Curve= len(verts) Do_Curve(0) faces.extend(Torx_Fill(FaceStart_Outside, 0)) - + FaceStart_Bottom_Curve= len(verts) Do_Curve(0 - HEIGHT) - + faces.extend(Build_Face_List_Quads(FaceStart_Top_Curve,42 ,1 , True)) - + verts.append([0,0,0 - HEIGHT]) # add center point for fill Fan faces.extend(Fill_Fan_Face(FaceStart_Bottom_Curve, 44)) - + M_Verts, M_Faces = Mirror_Verts_Faces(verts, faces, 'x') verts.extend(M_Verts) faces.extend(M_Faces) - + M_Verts, M_Faces = Mirror_Verts_Faces(verts, faces, 'y') verts.extend(M_Verts) faces.extend(M_Faces) @@ -1078,12 +1078,12 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): FLANGE_HEIGHT = (1.89/8.0)*HEIGHT FLAT_HEIGHT = (4.18/8.0)*HEIGHT # FLANGE_DIA = (13.27/8.0)*FLAT - + FLANGE_RADIUS = FLANGE_DIA * 0.5 - FLANGE_TAPPER_HEIGHT = HEIGHT - FLANGE_HEIGHT - FLAT_HEIGHT - + FLANGE_TAPPER_HEIGHT = HEIGHT - FLANGE_HEIGHT - FLAT_HEIGHT + # HOLE_DIA = 0.0 - + verts = [] faces = [] HOLE_RADIUS = HOLE_DIA / 2 @@ -1098,7 +1098,7 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): verts.append([0.0, 0.0, 0.0]) -# print("HOLE_RADIUS" + str(HOLE_RADIUS)) +# print("HOLE_RADIUS" + str(HOLE_RADIUS)) # print("TopBevelRadius" + str(TopBevelRadius)) FaceStart = len(verts) @@ -1123,15 +1123,15 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): x = sin(radians(20)) * HOLE_RADIUS y = cos(radians(20)) * HOLE_RADIUS verts.append([x, y, 0.0]) - + x = sin(radians(25)) * HOLE_RADIUS y = cos(radians(25)) * HOLE_RADIUS verts.append([x, y, 0.0]) - + x = sin(radians(30)) * HOLE_RADIUS y = cos(radians(30)) * HOLE_RADIUS verts.append([x, y, 0.0]) - + Row += 1 @@ -1156,17 +1156,17 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): y = cos(radians(15)) * TopBevelRadius vec4 = Vector([x, y, 0.0]) verts.append([x, y, 0.0]) - + x = sin(radians(20)) * TopBevelRadius y = cos(radians(20)) * TopBevelRadius vec5 = Vector([x, y, 0.0]) verts.append([x, y, 0.0]) - + x = sin(radians(25)) * TopBevelRadius y = cos(radians(25)) * TopBevelRadius vec6 = Vector([x, y, 0.0]) verts.append([x, y, 0.0]) - + x = sin(radians(30)) * TopBevelRadius y = cos(radians(30)) * TopBevelRadius vec7 = Vector([x, y, 0.0]) @@ -1176,11 +1176,11 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): #45Deg bevel on the top - + #First we work out how far up the Y axis the vert is v_origin = Vector([0.0,0.0,0.0]) # center of the model - v_15Deg_Point = Vector([tan(radians(15)) * Half_Flat,Half_Flat,0.0]) #Is a know point to work back from - + v_15Deg_Point = Vector([tan(radians(15)) * Half_Flat,Half_Flat,0.0]) #Is a know point to work back from + x = tan(radians(0)) * Half_Flat Point_Distance =(tan(radians(30)) * v_15Deg_Point.x)+Half_Flat dvec = vec1 - Vector([x, Point_Distance, 0.0]) @@ -1188,18 +1188,18 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): v_0_Deg_Top_Point = Vector([x, Point_Distance, -dvec.length]) v_0_Deg_Point = Vector([x, Point_Distance,0.0]) - + v_5Deg_Line = Vector([tan(radians(5)) * Half_Flat, Half_Flat, 0.0]) v_5Deg_Line.length *= 2 # extende out the line on a 5 deg angle #We cross 2 lines. One from the origin to the 0 Deg point #and the second is from the orign extended out past the first line - # This gives the cross point of the + # This gives the cross point of the v_Cross = geometry.intersect_line_line_2d(v_0_Deg_Point,v_15Deg_Point,v_origin,v_5Deg_Line) dvec = vec2 - Vector([v_Cross.x,v_Cross.y,0.0]) verts.append([v_Cross.x,v_Cross.y,-dvec.length]) v_5_Deg_Top_Point = Vector([v_Cross.x,v_Cross.y,-dvec.length]) - + v_10Deg_Line = Vector([tan(radians(10)) * Half_Flat, Half_Flat, 0.0]) v_10Deg_Line.length *= 2 # extende out the line @@ -1207,14 +1207,14 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): dvec = vec3 - Vector([v_Cross.x,v_Cross.y,0.0]) verts.append([v_Cross.x,v_Cross.y,-dvec.length]) v_10_Deg_Top_Point = Vector([v_Cross.x,v_Cross.y,-dvec.length]) - - #The remain points are stright forward because y is all the same y height (Half_Flat) + + #The remain points are stright forward because y is all the same y height (Half_Flat) x = tan(radians(15)) * Half_Flat dvec = vec4 - Vector([x, Half_Flat, 0.0]) Lowest_Point = -dvec.length verts.append([x, Half_Flat, -dvec.length]) v_15_Deg_Top_Point = Vector([x, Half_Flat, -dvec.length]) - + x = tan(radians(20)) * Half_Flat dvec = vec5 - Vector([x, Half_Flat, 0.0]) Lowest_Point = -dvec.length @@ -1234,43 +1234,43 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): v_30_Deg_Top_Point = Vector([x, Half_Flat, -dvec.length]) Row += 1 - + #Down Bits # print ("Point_Distance") # print (Point_Distance) - + Flange_Adjacent = FLANGE_RADIUS - Point_Distance if (Flange_Adjacent == 0.0): Flange_Adjacent = 0.000001 - Flange_Opposite = FLANGE_TAPPER_HEIGHT - + Flange_Opposite = FLANGE_TAPPER_HEIGHT + # print ("Flange_Opposite") # print (Flange_Opposite) # print ("Flange_Adjacent") # print (Flange_Adjacent) - + FLANGE_ANGLE_RAD = atan(Flange_Opposite/Flange_Adjacent ) # FLANGE_ANGLE_RAD = radians(45) # print("FLANGE_ANGLE_RAD") -# print (degrees (FLANGE_ANGLE_RAD)) - - +# print (degrees (FLANGE_ANGLE_RAD)) + + v_Extended_Flange_Edge = Vector([0.0,0.0,-HEIGHT + FLANGE_HEIGHT + (tan(FLANGE_ANGLE_RAD)* FLANGE_RADIUS) ]) # print("v_Extended_Flange_Edge") -# print (v_Extended_Flange_Edge) +# print (v_Extended_Flange_Edge) #0deg v_Flange_Edge = Vector([sin(radians(0)) * FLANGE_RADIUS,cos(radians(0)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT ]) v_Cross = geometry.intersect_line_line(v_0_Deg_Top_Point,Vector([v_0_Deg_Top_Point.x,v_0_Deg_Top_Point.y,-HEIGHT]),v_Flange_Edge,v_Extended_Flange_Edge) verts.append(v_Cross[0]) - + #5deg v_Flange_Edge = Vector([sin(radians(5)) * FLANGE_RADIUS,cos(radians(5)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT ]) v_Cross = geometry.intersect_line_line(v_5_Deg_Top_Point,Vector([v_5_Deg_Top_Point.x,v_5_Deg_Top_Point.y,-HEIGHT]),v_Flange_Edge,v_Extended_Flange_Edge) verts.append(v_Cross[0]) - + #10deg v_Flange_Edge = Vector([sin(radians(10)) * FLANGE_RADIUS,cos(radians(10)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT ]) v_Cross = geometry.intersect_line_line(v_10_Deg_Top_Point,Vector([v_10_Deg_Top_Point.x,v_10_Deg_Top_Point.y,-HEIGHT]),v_Flange_Edge,v_Extended_Flange_Edge) @@ -1286,13 +1286,13 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): v_Flange_Edge = Vector([sin(radians(20)) * FLANGE_RADIUS,cos(radians(20)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT ]) v_Cross = geometry.intersect_line_line(v_20_Deg_Top_Point,Vector([v_20_Deg_Top_Point.x,v_20_Deg_Top_Point.y,-HEIGHT]),v_Flange_Edge,v_Extended_Flange_Edge) verts.append(v_Cross[0]) - + #25deg v_Flange_Edge = Vector([sin(radians(25)) * FLANGE_RADIUS,cos(radians(25)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT ]) v_Cross = geometry.intersect_line_line(v_25_Deg_Top_Point,Vector([v_25_Deg_Top_Point.x,v_25_Deg_Top_Point.y,-HEIGHT]),v_Flange_Edge,v_Extended_Flange_Edge) verts.append(v_Cross[0]) - - + + #30deg v_Flange_Edge = Vector([sin(radians(30)) * FLANGE_RADIUS,cos(radians(30)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT ]) v_Cross = geometry.intersect_line_line(v_30_Deg_Top_Point,Vector([v_30_Deg_Top_Point.x,v_30_Deg_Top_Point.y,-HEIGHT]),v_Flange_Edge,v_Extended_Flange_Edge) @@ -1309,7 +1309,7 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): verts.append([sin(radians(20)) * FLANGE_RADIUS,cos(radians(20)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT]) verts.append([sin(radians(25)) * FLANGE_RADIUS,cos(radians(25)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT]) verts.append([sin(radians(30)) * FLANGE_RADIUS,cos(radians(30)) * FLANGE_RADIUS,-HEIGHT + FLANGE_HEIGHT]) - + Row += 1 verts.append([sin(radians(0)) * FLANGE_RADIUS,cos(radians(0)) * FLANGE_RADIUS,-HEIGHT]) @@ -1319,10 +1319,10 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): verts.append([sin(radians(20)) * FLANGE_RADIUS,cos(radians(20)) * FLANGE_RADIUS,-HEIGHT]) verts.append([sin(radians(25)) * FLANGE_RADIUS,cos(radians(25)) * FLANGE_RADIUS,-HEIGHT]) verts.append([sin(radians(30)) * FLANGE_RADIUS,cos(radians(30)) * FLANGE_RADIUS,-HEIGHT]) - + Row += 1 - + verts.append([sin(radians(0)) * SHANK_RADIUS,cos(radians(0)) * SHANK_RADIUS,-HEIGHT]) verts.append([sin(radians(0)) * SHANK_RADIUS,cos(radians(0)) * SHANK_RADIUS,-HEIGHT]) verts.append([sin(radians(10)) * SHANK_RADIUS,cos(radians(10)) * SHANK_RADIUS,-HEIGHT]) @@ -1330,7 +1330,7 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): verts.append([sin(radians(20)) * SHANK_RADIUS,cos(radians(20)) * SHANK_RADIUS,-HEIGHT]) verts.append([sin(radians(20)) * SHANK_RADIUS,cos(radians(20)) * SHANK_RADIUS,-HEIGHT]) verts.append([sin(radians(30)) * SHANK_RADIUS,cos(radians(30)) * SHANK_RADIUS,-HEIGHT]) - + Row += 1 @@ -1344,8 +1344,8 @@ def Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): def Create_12_Point_Head(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA): #TODO add under head radius return Create_12_Point(FLAT, HOLE_DIA, SHANK_DIA, HEIGHT,FLANGE_DIA) - - + + # #################################################################### # Create External Thread @@ -2288,13 +2288,13 @@ def Nut_Mesh(props, context): Face_Start = len(verts) - - + + if props.bf_Nut_Type == 'bf_Nut_12Pnt': Nut_Height = props.bf_12_Point_Nut_Height else: Nut_Height = props.bf_Hex_Nut_Height - + Thread_Verts, Thread_Faces, New_Nut_Height = Create_Internal_Thread( props.bf_Minor_Dia, props.bf_Major_Dia, props.bf_Pitch, Nut_Height, @@ -2305,7 +2305,7 @@ def Nut_Mesh(props, context): faces.extend(Copy_Faces(Thread_Faces, Face_Start)) Face_Start = len(verts) - + if props.bf_Nut_Type == 'bf_Nut_12Pnt': Head_Verts, Head_Faces, Lock_Nut_Rad = add_12_Point_Nut( props.bf_12_Point_Nut_Flat_Distance, @@ -2397,13 +2397,13 @@ def Bolt_Mesh(props, context): props.bf_Hex_Head_Flat_Distance, Bit_Dia, props.bf_Shank_Dia, props.bf_Hex_Head_Height ) - + elif props.bf_Head_Type == 'bf_Head_12Pnt': Head_Verts, Head_Faces, Head_Height = Create_12_Point_Head( props.bf_12_Point_Head_Flat_Distance, Bit_Dia, props.bf_Shank_Dia, props.bf_12_Point_Head_Height, #Limit the size of the Flange to avoid calculation error - max(props.bf_12_Point_Head_Flange_Dia,props.bf_12_Point_Head_Flat_Distance) + max(props.bf_12_Point_Head_Flange_Dia,props.bf_12_Point_Head_Flat_Distance) ) elif props.bf_Head_Type == 'bf_Head_Cap': Head_Verts, Head_Faces, Head_Height = Create_Cap_Head( diff --git a/io_coat3D/__init__.py b/io_coat3D/__init__.py index 5d74a960..77318c74 100644 --- a/io_coat3D/__init__.py +++ b/io_coat3D/__init__.py @@ -139,7 +139,7 @@ def set_exchange_folder(): file.write("%s"%(coat3D.exchangedir)) file.close() exchange = coat3D.exchangedir - + else: exchange = source @@ -292,10 +292,10 @@ def updatemesh(objekti, proxy, texturelist): if(udim_textures): udim = proxy.data.uv_layers[index].name udim_index = int(udim[2:]) - 1 - + objekti.data.uv_layers[0].data[indi].uv[0] = proxy.data.uv_layers[index].data[indi].uv[0] objekti.data.uv_layers[0].data[indi].uv[1] = proxy.data.uv_layers[index].data[indi].uv[1] - + index = index + 1 # Mesh Copy @@ -312,11 +312,11 @@ class SCENE_OT_getback(bpy.types.Operator): bl_options = {'UNDO'} def invoke(self, context, event): - + global global_exchange_folder global initial_settings path_ex = '' - + if(initial_settings): global_exchange_folder = set_exchange_folder() initial_settings = False @@ -326,29 +326,29 @@ class SCENE_OT_getback(bpy.types.Operator): BlenderFolder = Blender_folder ExportFolder = Export_folder - + Blender_folder += ('%sexport.txt' % (os.sep)) Export_folder += ('%sexport.txt' % (os.sep)) - + if (bpy.app.background == False): if os.path.isfile(Export_folder): print('BLENDER -> 3DC -> BLENDER WORKFLLOW') - DeleteExtra3DC() + DeleteExtra3DC() workflow1(ExportFolder) removeFile(Export_folder) - removeFile(Blender_folder) - - - + removeFile(Blender_folder) + + + elif os.path.isfile(Blender_folder): print('3DC -> BLENDER WORKFLLOW') - DeleteExtra3DC() + DeleteExtra3DC() workflow2(BlenderFolder) removeFile(Blender_folder) - - + + return {'FINISHED'} @@ -576,7 +576,7 @@ class SCENE_OT_export(bpy.types.Operator): def invoke(self, context, event): bpy.ops.export_applink.pilgway_3d_coat() - + return {'FINISHED'} def execute(self, context): @@ -823,12 +823,12 @@ class SCENE_OT_export(bpy.types.Operator): if(node.name.startswith('3DC_') == True): material.material.node_tree.nodes.remove(node) - + for ind, mat_list in enumerate(mod_mat_list): if(mat_list == '__' + objekti.name): for ind, mat in enumerate(mod_mat_list[mat_list]): objekti.material_slots[mod_mat_list[mat_list][ind][0]].material = mod_mat_list[mat_list][ind][1] - + bpy.context.scene.render.engine = active_render return {'FINISHED'} @@ -855,7 +855,7 @@ def DeleteExtra3DC(): bpy.data.images.remove(del_img) bpy.data.materials.remove(material) - + image_del_list = [] for image in bpy.data.images: if (image.name.startswith('3DC')): @@ -934,7 +934,7 @@ def new_ref_function(new_applink_address, nimi): def blender_3DC_blender(texturelist): - + coat3D = bpy.context.scene.coat3D old_materials = bpy.data.materials.keys() @@ -1086,7 +1086,7 @@ def blender_3DC_blender(texturelist): #delete_materials_from_end(keep_materials_count, obj_proxy) - + updatemesh(objekti,obj_proxy, texturelist) bpy.context.view_layer.objects.active = objekti @@ -1213,7 +1213,7 @@ def blender_3DC(texturelist, new_applink_address): old_materials = bpy.data.materials.keys() old_objects = bpy.data.objects.keys() - + bpy.ops.import_scene.fbx(filepath=new_applink_address, global_scale = 1, axis_forward='-Z', axis_up='Y') new_materials = bpy.data.materials.keys() @@ -1288,7 +1288,7 @@ def blender_3DC(texturelist, new_applink_address): os.remove(Blender_export) if (os.path.isfile(Blender_folder2)): os.remove(Blender_folder2) - + for material in bpy.data.materials: if material.use_nodes == True: for node in material.node_tree.nodes: @@ -1309,9 +1309,9 @@ def workflow1(ExportFolder): for image in bpy.data.images: if(image.filepath == texturepath[3] and image.users == 0): bpy.data.images.remove(image) - + path3b_now = coat3D.exchangedir - + path3b_now += ('last_saved_3b_file.txt') new_applink_address = 'False' new_object = False @@ -1335,7 +1335,7 @@ def workflow1(ExportFolder): new_ref_object = True nimi = scene_objects.name - + exportfile = coat3D.exchangedir @@ -1368,9 +1368,9 @@ def workflow2(BlenderFolder): kokeilu = coat3D.exchangedir Blender_export = os.path.join(kokeilu, 'Blender') - + path3b_now = coat3D.exchangedir - + path3b_now += ('last_saved_3b_file.txt') Blender_export += ('%sexport.txt'%(os.sep)) new_applink_address = 'False' @@ -2058,7 +2058,7 @@ def register(): bpy.types.Object.coat3D = PointerProperty(type=ObjectCoat3D) bpy.types.Scene.coat3D = PointerProperty(type=SceneCoat3D) bpy.types.Mesh.coat3D = PointerProperty(type=MeshCoat3D) - bpy.types.Material.coat3D = PointerProperty(type=MaterialCoat3D) + bpy.types.Material.coat3D = PointerProperty(type=MaterialCoat3D) kc = bpy.context.window_manager.keyconfigs.addon diff --git a/io_coat3D/tex.py b/io_coat3D/tex.py index 3a48ab8f..21fa92b3 100644 --- a/io_coat3D/tex.py +++ b/io_coat3D/tex.py @@ -128,22 +128,22 @@ def updatetextures(objekti): # Update 3DC textures def testi(objekti, texture_info, index_mat_name, uv_MODE_mat, mat_index): if uv_MODE_mat == 'UV': - + uv_set_founded = False for uvset in objekti.data.uv_layers: - + if(uvset.name == texture_info): uv_set_founded = True - + break - + if(uv_set_founded): for uv_poly in objekti.data.uv_layers[texture_info].id_data.polygons: if(mat_index == uv_poly.material_index): return True else: return False - + elif uv_MODE_mat == 'MAT': return (texture_info == index_mat_name) @@ -168,17 +168,17 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r if(udim_textures == False): for slot_index, texture_info in enumerate(texturelist): - uv_MODE_mat = 'MAT' + uv_MODE_mat = 'MAT' for index, layer in enumerate(objekti.data.uv_layers): if(layer.name == texturelist[slot_index][0]): uv_MODE_mat = 'UV' break - + if(testi(objekti, texturelist[slot_index][0], index_mat.name, uv_MODE_mat, ind)) : if texture_info[2] == 'color' or texture_info[2] == 'diffuse': if(index_mat.material.coat3D_diffuse): - + texcoat['color'].append(texture_info[3]) create_nodes = True else: @@ -241,7 +241,7 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r os.remove(texture_info[3]) create_group_node = True - + else: for texture_info in texturelist: @@ -516,21 +516,21 @@ def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, load_image = True for image in bpy.data.images: - + if(texcoat[type['name']][0] == image.filepath): load_image = False node.image = image - + break if (load_image): print('load_image', texcoat[type['name']][0]) - + node.image = bpy.data.images.load(texcoat[type['name']][0]) if(udim_textures): node.image.source = 'TILED' - + if node.image and type['colorspace'] == 'noncolor': node.image.colorspace_settings.is_data = True @@ -589,7 +589,7 @@ def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, if(material.name == '3DC_Emission'): main_material.links.new(applink_tree.outputs[type['input']], material.inputs[0]) break - + uv_node.location = node.location uv_node.location[0] -= 300 uv_node.location[1] -= 200 @@ -688,7 +688,7 @@ def matlab(objekti,mat_list,texturelist,is_new): ''' Check if bind textures with UVs or Materials ''' if(texturelist != []): - + udim_textures = False if texturelist[0][0].startswith('100') and len(texturelist[0][0]) == 4: udim_textures = True diff --git a/io_coat3D/texVR.py b/io_coat3D/texVR.py index 9f0e65ec..520b3084 100644 --- a/io_coat3D/texVR.py +++ b/io_coat3D/texVR.py @@ -173,19 +173,19 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r if(udim_textures == False): for slot_index, texture_info in enumerate(texturelist): - uv_MODE_mat = 'MAT' + uv_MODE_mat = 'MAT' for index, layer in enumerate(objekti.data.uv_layers): if(layer.name == texturelist[slot_index][0]): uv_MODE_mat = 'UV' break - + print('aaa') print(texture_info[0]) print(index_mat) if(texture_info[0] == index_mat.name): if texture_info[2] == 'color' or texture_info[2] == 'diffuse': if(index_mat.material.coat3D_diffuse): - + texcoat['color'].append(texture_info[3]) create_nodes = True else: @@ -248,7 +248,7 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r os.remove(texture_info[3]) create_group_node = True - + else: for texture_info in texturelist: @@ -282,7 +282,7 @@ def readtexturefolder(objekti, mat_list, texturelist, is_new, udim_textures): #r texcoat['alpha'].append([texture_info[0], texture_info[3]]) create_nodes = True create_group_node = True - + if(create_nodes): coat3D = bpy.context.scene.coat3D path3b_n = coat3D.exchangedir @@ -311,7 +311,7 @@ def createnodes(active_mat,texcoat, create_group_node, tile_list, objekti, ind, if(coatMat.use_nodes == False): coatMat.use_nodes = True - + act_material = coatMat.node_tree main_material = coatMat.node_tree applink_group_node = False @@ -596,7 +596,7 @@ def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, else: node = act_material.nodes.new('ShaderNodeTexImage') uv_node = act_material.nodes.new('ShaderNodeUVMap') - + uv_node.uv_map = objekti.data.uv_layers[0].name act_material.links.new(uv_node.outputs[0], node.inputs[0]) uv_node.use_custom_color = True @@ -646,7 +646,7 @@ def CreateTextureLine(type, act_material, main_mat, texcoat, coat3D, notegroup, if (load_image): node.image = bpy.data.images.load(texcoat[type['name']][0]) - + if node.image and type['colorspace'] == 'noncolor': node.image.colorspace_settings.is_data = True @@ -788,7 +788,7 @@ def createExtraNodes(act_material, node, type, notegroup): curvenode.location = -900, -1250 rampnode.location = -600, -1200 huenode.location = -300, -1200 - + if(type['name'] == 'color'): node_vertex = act_material.nodes.new('ShaderNodeVertexColor') node_mixRGB = act_material.nodes.new('ShaderNodeMixRGB') @@ -804,12 +804,12 @@ def createExtraNodes(act_material, node, type, notegroup): node_vertex.location = -337, 525 node_mixRGB.location = 0, 425 - + act_material.links.new(node_vertex.outputs[0], node_mixRGB.inputs[1]) act_material.links.new(huenode.outputs[0], node_mixRGB.inputs[2]) act_material.links.new(node_vertex.outputs[1], notegroup.inputs[8]) act_material.links.new(node_mixRGB.outputs[0], node_vectormath.inputs[0]) - + return node_vectormath return huenode @@ -829,7 +829,7 @@ def matlab(objekti,mat_list,texturelist,is_new): ''' Check if bind textures with UVs or Materials ''' if(texturelist != []): - + udim_textures = False if texturelist[0][0].startswith('100'): udim_textures = True diff --git a/oscurart_tools/__init__.py b/oscurart_tools/__init__.py index 0d340cc8..f630b456 100644 --- a/oscurart_tools/__init__.py +++ b/oscurart_tools/__init__.py @@ -76,7 +76,7 @@ class VIEW3D_MT_edit_mesh_oscurarttools(Menu): layout.operator("mesh.uv_island_copy") layout.operator("mesh.uv_island_paste") layout.operator("mesh.select_doubles") - layout.operator("mesh.print_uv_stats") + layout.operator("mesh.print_uv_stats") layout.separator() layout.operator("image.reload_images_osc") layout.operator("file.save_incremental_backup") @@ -124,7 +124,7 @@ class VIEW3D_MT_object_oscurarttools(Menu): layout.operator("object.search_and_select_osc") layout.operator("object.shape_key_to_objects_osc") layout.operator("mesh.apply_linked_meshes") - layout.operator("mesh.print_uv_stats") + layout.operator("mesh.print_uv_stats") layout.separator() layout.operator("image.reload_images_osc") layout.operator("file.save_incremental_backup") diff --git a/oscurart_tools/mesh/overlap_uvs.py b/oscurart_tools/mesh/overlap_uvs.py index c5bbab05..8b2a893c 100644 --- a/oscurart_tools/mesh/overlap_uvs.py +++ b/oscurart_tools/mesh/overlap_uvs.py @@ -27,8 +27,8 @@ from bpy.props import ( FloatProperty, EnumProperty, ) - - + + import bmesh # -------------------------- OVERLAP UV ISLANDS @@ -37,7 +37,7 @@ def defCopyUvsIsland(self, context): global islandSet islandSet = {} islandSet["Loop"] = [] - + bpy.context.scene.tool_settings.use_uv_select_sync = True bpy.ops.uv.select_linked() bm = bmesh.from_edit_mesh(bpy.context.object.data) @@ -45,39 +45,39 @@ def defCopyUvsIsland(self, context): faceSel = 0 for face in bm.faces: if face.select: - faceSel +=1 + faceSel +=1 for loop in face.loops: - islandSet["Loop"].append(loop[uv_lay].uv.copy()) + islandSet["Loop"].append(loop[uv_lay].uv.copy()) islandSet["Size"] = faceSel def defPasteUvsIsland(self, uvOffset, rotateUv,context): bm = bmesh.from_edit_mesh(bpy.context.object.data) - bpy.context.scene.tool_settings.use_uv_select_sync = True + bpy.context.scene.tool_settings.use_uv_select_sync = True pickedFaces = [face for face in bm.faces if face.select] for face in pickedFaces: bpy.ops.mesh.select_all(action="DESELECT") face.select=True bmesh.update_edit_mesh(bpy.context.object.data) bpy.ops.uv.select_linked() - uv_lay = bm.loops.layers.uv.active + uv_lay = bm.loops.layers.uv.active faceSel = 0 for face in bm.faces: if face.select: - faceSel +=1 - i = 0 - if faceSel == islandSet["Size"]: + faceSel +=1 + i = 0 + if faceSel == islandSet["Size"]: for face in bm.faces: - if face.select: + if face.select: for loop in face.loops: loop[uv_lay].uv = islandSet["Loop"][i] if uvOffset == False else islandSet["Loop"][i]+Vector((1,0)) - i += 1 + i += 1 else: - print("the island have a different size of geometry") - + print("the island have a different size of geometry") + if rotateUv: bpy.ops.object.mode_set(mode="EDIT") bmesh.ops.reverse_uvs(bm, faces=[f for f in bm.faces if f.select]) - bmesh.ops.rotate_uvs(bm, faces=[f for f in bm.faces if f.select]) + bmesh.ops.rotate_uvs(bm, faces=[f for f in bm.faces if f.select]) class CopyUvIsland(Operator): """Copy Uv Island""" @@ -110,7 +110,7 @@ class PasteUvIsland(Operator): name="Rotate Uv Corner", default=False ) - + @classmethod def poll(cls, context): return (context.active_object is not None and diff --git a/oscurart_tools/mesh/print_uv_stats.py b/oscurart_tools/mesh/print_uv_stats.py index 7488456d..6cc673a9 100644 --- a/oscurart_tools/mesh/print_uv_stats.py +++ b/oscurart_tools/mesh/print_uv_stats.py @@ -70,14 +70,14 @@ def calcMeshArea(ob): polyArea = 0 for poly in ob.data.polygons: polyArea += poly.area - ta = "UvGain: %s%s || " % (round(totalArea * 100),"%") + ta = "UvGain: %s%s || " % (round(totalArea * 100),"%") ma = "MeshArea: %s || " % (polyArea) pg = "PixelsGain: %s || " % (round(totalArea * (pixels[0] * pixels[1]))) pl = "PixelsLost: %s || " % ((pixels[0]*pixels[1]) - round(totalArea * (pixels[0] * pixels[1]))) - tx = "Texel: %s pix/meter" % (round(sqrt(totalArea * pixels[0] * pixels[1] / polyArea))) - GlobLog = ta+ma+pg+pl+tx + tx = "Texel: %s pix/meter" % (round(sqrt(totalArea * pixels[0] * pixels[1] / polyArea))) + GlobLog = ta+ma+pg+pl+tx + - class uvStats(bpy.types.Operator): @@ -90,7 +90,7 @@ class uvStats(bpy.types.Operator): def poll(cls, context): return context.active_object is not None - def execute(self, context): + def execute(self, context): if round( bpy.context.object.scale.x, 2) == 1 and round( @@ -101,25 +101,25 @@ class uvStats(bpy.types.Operator): if setImageRes(bpy.context.object): makeTessellate(bpy.context.object) calcArea() - calcMeshArea(bpy.context.object) + calcMeshArea(bpy.context.object) else: print("Warning: Non Uniform Scale Object") - + copyOb = bpy.context.object.copy() copyMe = bpy.context.object.data.copy() bpy.context.scene.collection.objects.link(copyOb) copyOb.data = copyMe bpy.ops.object.select_all(action="DESELECT") copyOb.select_set(1) - bpy.ops.object.transform_apply() - + bpy.ops.object.transform_apply() + if setImageRes(copyOb): makeTessellate(copyOb) calcArea() calcMeshArea(copyOb) - + bpy.data.objects.remove(copyOb) - bpy.data.meshes.remove(copyMe) - - self.report({'INFO'}, GlobLog) - return {'FINISHED'} \ No newline at end of file + bpy.data.meshes.remove(copyMe) + + self.report({'INFO'}, GlobLog) + return {'FINISHED'} diff --git a/oscurart_tools/render/material_overrides.py b/oscurart_tools/render/material_overrides.py index db200572..d8aade82 100644 --- a/oscurart_tools/render/material_overrides.py +++ b/oscurart_tools/render/material_overrides.py @@ -11,13 +11,13 @@ def ApplyOverrides(dummy): global obDict for override in bpy.context.scene.ovlist: - + # set collections clean name - collClean = override.colloverride + collClean = override.colloverride obClean = override.oboverride - - - + + + if collClean != None: for ob in collClean.all_objects: if ob.type == "MESH": #si es un mesh @@ -28,22 +28,22 @@ def ApplyOverrides(dummy): for iob in ob.instance_collection.all_objects: if iob.type == "MESH": if not iob.hide_viewport and not iob.hide_render: - obDict.append([iob,[mat for mat in iob.data.materials]]) + obDict.append([iob,[mat for mat in iob.data.materials]]) else: - obDict.append([obClean,[mat for mat in obClean.data.materials]]) - - + obDict.append([obClean,[mat for mat in obClean.data.materials]]) + + for override in bpy.context.scene.ovlist: - + # set collections clean name - collClean = override.colloverride - # set material clean name - matClean = override.matoverride - # set objeto clean name - obClean = override.oboverride - + collClean = override.colloverride + # set material clean name + matClean = override.matoverride + # set objeto clean name + obClean = override.oboverride + print(matClean) - + if collClean != None: for ob in collClean.all_objects: if ob.type == "MESH": @@ -56,12 +56,12 @@ def ApplyOverrides(dummy): if iob.type == "MESH": if not iob.hide_viewport and not iob.hide_render: for i,mat in enumerate(iob.data.materials): - iob.data.materials[i] = matClean + iob.data.materials[i] = matClean else: if obClean.type == "MESH": - if not obClean.hide_viewport and not obClean.hide_render: - for i,mat in enumerate(obClean.data.materials): - obClean.data.materials[i] = matClean + if not obClean.hide_viewport and not obClean.hide_render: + for i,mat in enumerate(obClean.data.materials): + obClean.data.materials[i] = matClean @persistent @@ -79,22 +79,22 @@ def RestoreOverrides(dummy): -class OscOverridesProp(bpy.types.PropertyGroup): +class OscOverridesProp(bpy.types.PropertyGroup): colloverride: bpy.props.PointerProperty( name="Collection Override", type=bpy.types.Collection, description="All objects in this collection will be override", - ) + ) oboverride: bpy.props.PointerProperty( name="Object Override", type=bpy.types.Object, description="Only this object will be override.", - ) + ) matoverride: bpy.props.PointerProperty( name="Material Override", type=bpy.types.Material, description="Material for override objects", - ) + ) bpy.utils.register_class(OscOverridesProp) bpy.types.Scene.ovlist = bpy.props.CollectionProperty(type=OscOverridesProp) @@ -118,9 +118,9 @@ class OVERRIDES_PT_OscOverridesGUI(bpy.types.Panel): col.operator("render.overrides_transfer") for i, m in enumerate(bpy.context.scene.ovlist): colrow = col.row(align=1) - colrow.prop(m, "colloverride", text="") - colrow.prop(m, "oboverride", text="") - colrow.prop(m, "matoverride", text="") + colrow.prop(m, "colloverride", text="") + colrow.prop(m, "oboverride", text="") + colrow.prop(m, "matoverride", text="") if i != len(bpy.context.scene.ovlist) - 1: pa = colrow.operator( "ovlist.move_down", diff --git a/oscurart_tools/render/render_tokens.py b/oscurart_tools/render/render_tokens.py index 724cc843..69c79ad8 100644 --- a/oscurart_tools/render/render_tokens.py +++ b/oscurart_tools/render/render_tokens.py @@ -33,16 +33,16 @@ def replaceTokens (dummy): "$Camera": "NoCamera" if bpy.context.scene.camera == None else bpy.context.scene.camera.name} renpath = bpy.context.scene.render.filepath - + nodeDict = [] #compositor nodes if bpy.context.scene.use_nodes: for node in bpy.context.scene.node_tree.nodes: if node.type == "OUTPUT_FILE": - nodeDict.append([node,node.base_path]) + nodeDict.append([node,node.base_path]) node.base_path = node.base_path.replace("$Scene",tokens["$Scene"]).replace("$File",tokens["$File"]).replace("$ViewLayer",tokens["$ViewLayer"]).replace("$Camera",tokens["$Camera"]) - - + + bpy.context.scene.render.filepath = renpath.replace("$Scene",tokens["$Scene"]).replace("$File",tokens["$File"]).replace("$ViewLayer",tokens["$ViewLayer"]).replace("$Camera",tokens["$Camera"]) print(bpy.context.scene.render.filepath) @@ -52,7 +52,7 @@ def replaceTokens (dummy): def restoreTokens (dummy): global renpath bpy.context.scene.render.filepath = renpath - + #restore nodes for node in nodeDict: node[0].base_path = node[1] diff --git a/power_sequencer/operators/scene_create_from_selection.py b/power_sequencer/operators/scene_create_from_selection.py index 57c6a6df..b615dae9 100644 --- a/power_sequencer/operators/scene_create_from_selection.py +++ b/power_sequencer/operators/scene_create_from_selection.py @@ -53,7 +53,7 @@ class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator): def execute(self, context): start_scene_name = context.scene.name - + if len(context.selected_sequences) != 0: selection = context.selected_sequences[:] selection_start_frame = min( @@ -63,11 +63,11 @@ class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator): # Create new scene for the scene strip bpy.ops.scene.new(type="FULL_COPY") - + context.window.scene.name = context.selected_sequences[0].name new_scene_name = context.window.scene.name - - + + ###after full copy also unselected strips are in the sequencer... Delete those strips bpy.ops.sequencer.select_all(action="INVERT") bpy.ops.power_sequencer.delete_direct() @@ -80,7 +80,7 @@ class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator): bpy.ops.sequencer.select_all() bpy.ops.power_sequencer.preview_to_selection() - # Back to start scene + # Back to start scene bpy.context.window.scene = bpy.data.scenes[start_scene_name] bpy.ops.power_sequencer.delete_direct() diff --git a/power_sequencer/scripts/BPSRender/bpsrender/__init__.py b/power_sequencer/scripts/BPSRender/bpsrender/__init__.py index 35a40273..f14cfb6a 100644 --- a/power_sequencer/scripts/BPSRender/bpsrender/__init__.py +++ b/power_sequencer/scripts/BPSRender/bpsrender/__init__.py @@ -14,4 +14,3 @@ # You should have received a copy of the GNU General Public License along with Power Sequencer. If # not, see . # - -- cgit v1.2.3 From 82ed41ec632483fa9260d90dae7afdf3192c509b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 5 Aug 2020 18:16:08 +0200 Subject: Update calls to `Scene.ray_cast()` to pass depsgraph instead of view layer This is in light of the change in Blender rBe03d53874dac5f. --- archipack/archipack_object.py | 2 +- archipack/archipack_wall2.py | 2 +- blenderkit/ui.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/archipack/archipack_object.py b/archipack/archipack_object.py index 8c908214..f513b506 100644 --- a/archipack/archipack_object.py +++ b/archipack/archipack_object.py @@ -259,7 +259,7 @@ class ArchipackDrawTool(ArchipackCollectionManager): view_vector_mouse = region_2d_to_vector_3d(region, rv3d, co2d) ray_origin_mouse = region_2d_to_origin_3d(region, rv3d, co2d) res, pos, normal, face_index, object, matrix_world = context.scene.ray_cast( - view_layer=context.view_layer, + depsgraph=context.view_layer.depsgraph, origin=ray_origin_mouse, direction=view_vector_mouse) return res, pos, normal, face_index, object, matrix_world diff --git a/archipack/archipack_wall2.py b/archipack/archipack_wall2.py index d9a486eb..53375992 100644 --- a/archipack/archipack_wall2.py +++ b/archipack/archipack_wall2.py @@ -1639,7 +1639,7 @@ class archipack_wall2(ArchipackObject, Manipulable, PropertyGroup): # prevent self intersect o.hide_viewport = True res, pos, normal, face_index, r, matrix_world = context.scene.ray_cast( - view_layer=context.view_layer, + depsgraph=context.view_layer.depsgraph, origin=p, direction=up) diff --git a/blenderkit/ui.py b/blenderkit/ui.py index 5333b65e..47bf1a51 100644 --- a/blenderkit/ui.py +++ b/blenderkit/ui.py @@ -927,7 +927,7 @@ def mouse_raycast(context, mx, my): vec = ray_target - ray_origin has_hit, snapped_location, snapped_normal, face_index, object, matrix = bpy.context.scene.ray_cast( - bpy.context.view_layer, ray_origin, vec) + bpy.context.view_layer.depsgraph, ray_origin, vec) # rote = mathutils.Euler((0, 0, math.pi)) randoffset = math.pi -- cgit v1.2.3 From 04c0573ee77cc955cf585a0d7a61163375eb57cd Mon Sep 17 00:00:00 2001 From: Bastien Montagne Date: Fri, 7 Aug 2020 16:38:40 +0200 Subject: Fix T78278: Cannot import some binary PLY file generated by Rhinos3D 6.0 Issue was that in binary file reading, python only recognize `'\n'` character as line separator... PLY seems to allow (or at least, use) other OS-related variants of lines terminators, so we have to implement our own line iterator for those cases... --- io_mesh_ply/__init__.py | 4 ++-- io_mesh_ply/import_ply.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/io_mesh_ply/__init__.py b/io_mesh_ply/__init__.py index 1a69c346..a3f08ebd 100644 --- a/io_mesh_ply/__init__.py +++ b/io_mesh_ply/__init__.py @@ -20,8 +20,8 @@ bl_info = { "name": "Stanford PLY format", - "author": "Bruce Merry, Campbell Barton", - "version": (2, 0, 0), + "author": "Bruce Merry, Campbell Barton", "Bastien Montagne" + "version": (2, 1, 0), "blender": (2, 90, 0), "location": "File > Import/Export", "description": "Import-Export PLY mesh data with UVs and vertex colors", diff --git a/io_mesh_ply/import_ply.py b/io_mesh_ply/import_ply.py index c118aa3c..d9d12d67 100644 --- a/io_mesh_ply/import_ply.py +++ b/io_mesh_ply/import_ply.py @@ -152,14 +152,45 @@ def read(filepath): invalid_ply = (None, None, None) with open(filepath, 'rb') as plyf: - signature = plyf.readline() + signature = plyf.peek(5) - if not signature.startswith(b'ply'): + if not signature.startswith(b'ply') or not len(signature) >= 5: print("Signature line was invalid") return invalid_ply + custom_line_sep = None + if signature[3] != ord(b'\n'): + if signature[3] != ord(b'\r'): + print("Unknown line separator") + return invalid_ply + if signature[4] == ord(b'\n'): + custom_line_sep = b"\r\n" + else: + custom_line_sep = b"\r" + + # Work around binary file reading only accepting "\n" as line separator. + plyf_header_line_iterator = lambda plyf: plyf + if custom_line_sep is not None: + def _plyf_header_line_iterator(plyf): + buff = plyf.peek(2**16) + while len(buff) != 0: + read_bytes = 0 + buff = buff.split(custom_line_sep) + for line in buff[:-1]: + read_bytes += len(line) + len(custom_line_sep) + if line.startswith(b'end_header'): + # Since reader code might (will) break iteration at this point, + # we have to ensure file is read up to here, yield, amd return... + plyf.read(read_bytes) + yield line + return + yield line + plyf.read(read_bytes) + buff = buff[-1] + plyf.peek(2**16) + plyf_header_line_iterator = _plyf_header_line_iterator + valid_header = False - for line in plyf: + for line in plyf_header_line_iterator(plyf): tokens = re.split(br'[ \r\n]+', line) if len(tokens) == 0: -- cgit v1.2.3 From a3f6d85220f05e1b4e4b053cd3fdb921acac6047 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 8 Aug 2020 14:40:33 +0200 Subject: glTF: bump version after recent change --- io_scene_gltf2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index d911c97f..bcdb9cf0 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 43), + "version": (1, 3, 44), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', -- cgit v1.2.3 From 7aa6363ee1fd1efb4f58419cbd6f916e10a2d109 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 8 Aug 2020 14:47:49 +0200 Subject: glTF exporter: Export joint indices as uint8 when possible --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_gather_primitive_attributes.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index bafd7501..4b65741c 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 4, 2), + "version": (1, 4, 3), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py index 8912d921..61adea89 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py @@ -161,9 +161,12 @@ def __gather_skins(blender_primitive, export_settings): # joints internal_joint = blender_primitive["attributes"][joint_id] + component_type = gltf2_io_constants.ComponentType.UnsignedShort + if max(internal_joint) < 256: + component_type = gltf2_io_constants.ComponentType.UnsignedByte joint = array_to_accessor( internal_joint, - component_type=gltf2_io_constants.ComponentType.UnsignedShort, + component_type, data_type=gltf2_io_constants.DataType.Vec4, ) attributes[joint_id] = joint -- cgit v1.2.3 From 7cc963d96c2dd4fc4102750b843f2ba8688c6f24 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 8 Aug 2020 14:54:58 +0200 Subject: glTF: fix rst doc Thanks nutti --- io_scene_gltf2/__init__.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index bcdb9cf0..e3cd2918 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 44), + "version": (1, 3, 45), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -114,10 +114,10 @@ class ExportGLTF2_Base: export_image_format: EnumProperty( name='Images', items=(('AUTO', 'Automatic', - 'Save PNGs as PNGs and JPEGs as JPEGs.\n' + 'Save PNGs as PNGs and JPEGs as JPEGs. ' 'If neither one, use PNG'), ('JPEG', 'JPEG Format (.jpg)', - 'Save images as JPEGs. (Images that need alpha are saved as PNGs though.)\n' + 'Save images as JPEGs. (Images that need alpha are saved as PNGs though.) ' 'Be aware of a possible loss in quality'), ), description=( @@ -276,8 +276,8 @@ class ExportGLTF2_Base: export_nla_strips: BoolProperty( name='Group by NLA Track', description=( - "When on, multiple actions become part of the same glTF animation if\n" - "they're pushed onto NLA tracks with the same name.\n" + "When on, multiple actions become part of the same glTF animation if " + "they're pushed onto NLA tracks with the same name. " "When off, all the currently assigned actions become one glTF animation" ), default=True @@ -868,8 +868,8 @@ class ImportGLTF2(Operator, ImportHelper): description=( 'The glTF format requires discontinuous normals, UVs, and ' 'other vertex attributes to be stored as separate vertices, ' - 'as required for rendering on typical graphics hardware.\n' - 'This option attempts to combine co-located vertices where possible.\n' + 'as required for rendering on typical graphics hardware. ' + 'This option attempts to combine co-located vertices where possible. ' 'Currently cannot combine verts with different normals' ), default=False, @@ -887,15 +887,15 @@ class ImportGLTF2(Operator, ImportHelper): name="Bone Dir", items=( ("BLENDER", "Blender (best for re-importing)", - "Good for re-importing glTFs exported from Blender.\n" + "Good for re-importing glTFs exported from Blender. " "Bone tips are placed on their local +Y axis (in glTF space)"), ("TEMPERANCE", "Temperance (average)", - "Decent all-around strategy.\n" - "A bone with one child has its tip placed on the local axis\n" + "Decent all-around strategy. " + "A bone with one child has its tip placed on the local axis " "closest to its child"), ("FORTUNE", "Fortune (may look better, less accurate)", - "Might look better than Temperance, but also might have errors.\n" - "A bone with one child has its tip placed at its child's root.\n" + "Might look better than Temperance, but also might have errors. " + "A bone with one child has its tip placed at its child's root. " "Non-uniform scalings may get messed up though, so beware"), ), description="Heuristic for placing bones. Tries to make bones pretty", @@ -906,7 +906,7 @@ class ImportGLTF2(Operator, ImportHelper): name='Guess Original Bind Pose', description=( 'Try to guess the original bind pose for skinned meshes from ' - 'the inverse bind matrices.\n' + 'the inverse bind matrices. ' 'When off, use default/rest pose as bind pose' ), default=True, -- cgit v1.2.3 From c0522fcf8f23e77c5c68c1099a7140839a8d7d64 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 8 Aug 2020 15:06:58 +0200 Subject: glTF: fix bad version number after merge --- io_scene_gltf2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 3e8bb712..545a3264 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 46), + "version": (1, 4, 4), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', -- cgit v1.2.3 From 9fd05ef46664a8e1e6455ceeea2ff3d7dce84506 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 8 Aug 2020 16:02:04 +0200 Subject: glTF importer: fix regression for primitives having different numbers of TEXCOORD_{n}s --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/imp/gltf2_blender_mesh.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index e3cd2918..3e8bb712 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 45), + "version": (1, 3, 46), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py index e393eb86..e13b9c8f 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -200,7 +200,7 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob): uvs = BinaryData.decode_accessor(gltf, prim.attributes['TEXCOORD_%d' % uv_i], cache=True) uvs = uvs[indices] else: - uvs = np.zeros((len(indices), 3), dtype=np.float32) + uvs = np.zeros((len(indices), 2), dtype=np.float32) loop_uvs[uv_i] = np.concatenate((loop_uvs[uv_i], uvs)) for col_i in range(num_cols): -- cgit v1.2.3 From 386bb5eaa473fb4934a60a90999bf10061ebad99 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 8 Aug 2020 16:07:50 +0200 Subject: glTF exporter: fix to generate valid file when zero-weight verts A better fix will come later --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/exp/gltf2_blender_extract.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 3e8bb712..5a177494 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 46), + "version": (1, 3, 47), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index c605a609..eef05044 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -286,6 +286,7 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert bones.append((joint, weight)) bones.sort(key=lambda x: x[1], reverse=True) bones = tuple(bones) + if not bones: bones = ((0, 1.0),) # HACK for verts with zero weight (#308) vert += (bones,) for shape_key in shape_keys: -- cgit v1.2.3 From 4b656b65f81d7af9f69a57351870d2eb46bbbb8f Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sat, 8 Aug 2020 16:17:59 +0200 Subject: glTF importer: add KHR_mesh_quantization support (was already the case, but not said) --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/io/imp/gltf2_io_gltf.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 5a177494..89478bd6 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 3, 47), + "version": (1, 3, 48), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/io/imp/gltf2_io_gltf.py b/io_scene_gltf2/io/imp/gltf2_io_gltf.py index c494e966..49eee2d5 100755 --- a/io_scene_gltf2/io/imp/gltf2_io_gltf.py +++ b/io_scene_gltf2/io/imp/gltf2_io_gltf.py @@ -48,6 +48,7 @@ class glTFImporter(): 'KHR_materials_unlit', 'KHR_texture_transform', 'KHR_materials_clearcoat', + 'KHR_mesh_quantization', ] # TODO : merge with io_constants -- cgit v1.2.3 From fe1dc5a06f262d4906fcba7f73c75d2c371b3a1e Mon Sep 17 00:00:00 2001 From: "Vladimir Spivak(cwolf3d)" Date: Sun, 9 Aug 2020 12:14:47 +0300 Subject: Fix T78969 Addon: Add curve extra objects. NURBS mode, Order U changes nothing --- add_curve_extra_objects/add_curve_spirals.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/add_curve_extra_objects/add_curve_spirals.py b/add_curve_extra_objects/add_curve_spirals.py index 2ccdc51a..aa879007 100644 --- a/add_curve_extra_objects/add_curve_spirals.py +++ b/add_curve_extra_objects/add_curve_spirals.py @@ -245,19 +245,6 @@ def draw_curve(props, context, align_matrix): Curve.rotation_euler = props.rotation_euler Curve.select_set(True) - # set curveOptions - Curve.data.dimensions = props.shape - Curve.data.use_path = True - if props.shape == '3D': - Curve.data.fill_mode = 'FULL' - else: - Curve.data.fill_mode = 'BOTH' - - # set curveOptions - newSpline.use_cyclic_u = props.use_cyclic_u - newSpline.use_endpoint_u = props.endp_u - newSpline.order_u = props.order_u - # turn verts into array vertArray = vertsToPoints(verts, splineType) @@ -288,6 +275,19 @@ def draw_curve(props, context, align_matrix): for point in newSpline.points: point.select = True + # set curveOptions + newSpline.use_cyclic_u = props.use_cyclic_u + newSpline.use_endpoint_u = props.endp_u + newSpline.order_u = props.order_u + + # set curveOptions + Curve.data.dimensions = props.shape + Curve.data.use_path = True + if props.shape == '3D': + Curve.data.fill_mode = 'FULL' + else: + Curve.data.fill_mode = 'BOTH' + # move and rotate spline in edit mode if bpy.context.mode == 'EDIT_CURVE': bpy.ops.transform.translate(value = props.startlocation) -- cgit v1.2.3 From 663c7eb7265493d266908f7137c70479f3622316 Mon Sep 17 00:00:00 2001 From: "Vladimir Spivak(cwolf3d)" Date: Sun, 9 Aug 2020 13:38:14 +0300 Subject: Fix T78335 Bsurfaces GPL 1.7.8 : new patches, made with cross-hatching are shifted, if origin not in the center of world. --- mesh_bsurfaces.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mesh_bsurfaces.py b/mesh_bsurfaces.py index 9e7f7a59..5e3a601c 100644 --- a/mesh_bsurfaces.py +++ b/mesh_bsurfaces.py @@ -20,7 +20,7 @@ bl_info = { "name": "Bsurfaces GPL Edition", "author": "Eclectiel, Vladimir Spivak (cwolf3d)", - "version": (1, 7, 8), + "version": (1, 7, 9), "blender": (2, 80, 0), "location": "View3D EditMode > Sidebar > Edit Tab", "description": "Modeling and retopology tool", @@ -237,6 +237,9 @@ class MESH_OT_SURFSK_add_surface(Operator): bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges" bl_options = {'REGISTER', 'UNDO'} + is_crosshatch: BoolProperty( + default=False + ) is_fill_faces: BoolProperty( default=False ) @@ -1435,6 +1438,9 @@ class MESH_OT_SURFSK_add_surface(Operator): me.from_pydata(all_verts_coords, all_edges, []) ob = object_utils.object_data_add(context, me) + ob.location = (0.0, 0.0, 0.0) + ob.rotation_euler = (0.0, 0.0, 0.0) + ob.scale = (1.0, 1.0, 1.0) bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT') ob.select_set(True) @@ -1607,6 +1613,9 @@ class MESH_OT_SURFSK_add_surface(Operator): me_surf = bpy.data.meshes.new(surf_me_name) me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces) ob_surface = object_utils.object_data_add(context, me_surf) + ob_surface.location = (0.0, 0.0, 0.0) + ob_surface.rotation_euler = (0.0, 0.0, 0.0) + ob_surface.scale = (1.0, 1.0, 1.0) # Delete final points temporal object bpy.ops.object.delete({"selected_objects": [final_points_ob]}) -- cgit v1.2.3 From 7084d19ef5886b740b65ba6a4234849859e0e5b8 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Mon, 10 Aug 2020 02:39:43 -0400 Subject: Collection Manager: Fix regression. Task: T69577 Fix error when adding/removing objects caused by a regression from rBAadac42a46334 (improve performance for large numbers of selected objects) --- object_collection_manager/__init__.py | 2 +- object_collection_manager/operators.py | 2 +- object_collection_manager/qcd_operators.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 4fe792bb..09cb1c2c 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 4), + "version": (2, 12, 5), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 08c90224..46e83023 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -248,7 +248,7 @@ class CMSetCollectionOperator(Operator): # make sure there is an active object if not active_object: - active_object = selected_objects[0] + active_object = tuple(selected_objects)[0] # check if in collection if not active_object.name in target_collection.objects: diff --git a/object_collection_manager/qcd_operators.py b/object_collection_manager/qcd_operators.py index 56df1501..4f5f555a 100644 --- a/object_collection_manager/qcd_operators.py +++ b/object_collection_manager/qcd_operators.py @@ -81,7 +81,7 @@ class MoveToQCDSlot(Operator): # adds object to slot if self.toggle: if not active_object: - active_object = selected_objects[0] + active_object = tuple(selected_objects)[0] if not active_object.name in qcd_laycol.collection.objects: for obj in selected_objects: -- cgit v1.2.3 From 8db46434a4b25569372a7172f83733ec14dffb31 Mon Sep 17 00:00:00 2001 From: nutti Date: Mon, 10 Aug 2020 16:07:26 +0900 Subject: Magic UV: Release v6.3 Added features - Clip UV Updated features - World Scale UV * Add option "Area Calculation Method" * Add option "Only Selected" - UVW * Support multiple objects - Select UV * Support multiple objects - UV Inspection * Add Paint UV Island feature * Support multiple objects Other updates - Fix bugs - Optimization --- magic_uv/__init__.py | 10 +- magic_uv/common.py | 220 +++++++++----- magic_uv/lib/__init__.py | 4 +- magic_uv/op/__init__.py | 6 +- magic_uv/op/align_uv.py | 10 +- magic_uv/op/align_uv_cursor.py | 4 +- magic_uv/op/clip_uv.py | 227 ++++++++++++++ magic_uv/op/copy_paste_uv.py | 11 +- magic_uv/op/copy_paste_uv_object.py | 4 +- magic_uv/op/copy_paste_uv_uvedit.py | 4 +- magic_uv/op/flip_rotate_uv.py | 4 +- magic_uv/op/mirror_uv.py | 4 +- magic_uv/op/move_uv.py | 63 ++-- magic_uv/op/pack_uv.py | 4 +- magic_uv/op/preserve_uv_aspect.py | 4 +- magic_uv/op/select_uv.py | 83 ++++-- magic_uv/op/smooth_uv.py | 10 +- magic_uv/op/texture_lock.py | 6 +- magic_uv/op/texture_projection.py | 4 +- magic_uv/op/texture_wrap.py | 4 +- magic_uv/op/transfer_uv.py | 4 +- magic_uv/op/unwrap_constraint.py | 4 +- magic_uv/op/uv_bounding_box.py | 10 +- magic_uv/op/uv_inspection.py | 249 ++++++++++++++-- magic_uv/op/uv_sculpt.py | 24 +- magic_uv/op/uvw.py | 64 ++-- magic_uv/op/world_scale_uv.py | 394 +++++++++++++++++++------ magic_uv/preferences.py | 15 +- magic_uv/properites.py | 4 +- magic_uv/ui/IMAGE_MT_uvs.py | 14 +- magic_uv/ui/VIEW3D_MT_object.py | 4 +- magic_uv/ui/VIEW3D_MT_uv_map.py | 17 +- magic_uv/ui/__init__.py | 4 +- magic_uv/ui/uvedit_copy_paste_uv.py | 4 +- magic_uv/ui/uvedit_editor_enhancement.py | 7 +- magic_uv/ui/uvedit_uv_manipulation.py | 18 +- magic_uv/ui/view3d_copy_paste_uv_editmode.py | 4 +- magic_uv/ui/view3d_copy_paste_uv_objectmode.py | 4 +- magic_uv/ui/view3d_uv_manipulation.py | 114 +++---- magic_uv/ui/view3d_uv_mapping.py | 4 +- magic_uv/updater.py | 4 +- magic_uv/utils/__init__.py | 4 +- magic_uv/utils/addon_updater.py | 8 +- magic_uv/utils/bl_class_registry.py | 4 +- magic_uv/utils/compatibility.py | 4 +- magic_uv/utils/property_class_registry.py | 4 +- 46 files changed, 1235 insertions(+), 441 deletions(-) create mode 100644 magic_uv/op/clip_uv.py diff --git a/magic_uv/__init__.py b/magic_uv/__init__.py index 8630038a..b92714fe 100644 --- a/magic_uv/__init__.py +++ b/magic_uv/__init__.py @@ -20,21 +20,23 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" bl_info = { "name": "Magic UV", "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs" "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, " - "Alexander Milovsky", - "version": (6, 2, 0), + "Alexander Milovsky, Dusan Stevanovic", + "version": (6, 3, 0), "blender": (2, 80, 0), "location": "See Add-ons Preferences", "description": "UV Toolset. See Add-ons Preferences for details", "warning": "", "support": "COMMUNITY", + "wiki_url": "https://docs.blender.org/manual/en/dev/addons/" + "uv/magic_uv.html", "doc_url": "{BLENDER_MANUAL_URL}/addons/uv/magic_uv.html", "tracker_url": "https://github.com/nutti/Magic-UV", "category": "UV", diff --git a/magic_uv/common.py b/magic_uv/common.py index df3597be..11696667 100644 --- a/magic_uv/common.py +++ b/magic_uv/common.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from collections import defaultdict from pprint import pprint @@ -244,15 +244,16 @@ def __parse_island(bm, face_idx, faces_left, island, Parse island """ - if face_idx in faces_left: - faces_left.remove(face_idx) - island.append({'face': bm.faces[face_idx]}) - for v in face_to_verts[face_idx]: - connected_faces = vert_to_faces[v] - if connected_faces: + faces_to_parse = [face_idx] + while faces_to_parse: + fidx = faces_to_parse.pop(0) + if fidx in faces_left: + faces_left.remove(fidx) + island.append({'face': bm.faces[fidx]}) + for v in face_to_verts[fidx]: + connected_faces = vert_to_faces[v] for cf in connected_faces: - __parse_island(bm, cf, faces_left, island, face_to_verts, - vert_to_faces) + faces_to_parse.append(cf) def __get_island(bm, face_to_verts, vert_to_faces): @@ -351,18 +352,60 @@ def calc_polygon_3d_area(points): return 0.5 * area -def measure_mesh_area(obj): +def get_faces_list(bm, method, only_selected): + faces_list = [] + if method == 'MESH': + if only_selected: + faces_list.append([f for f in bm.faces if f.select]) + else: + faces_list.append([f for f in bm.faces]) + elif method == 'UV ISLAND': + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + if only_selected: + faces = [f for f in bm.faces if f.select] + islands = get_island_info_from_faces(bm, faces, uv_layer) + for isl in islands: + faces_list.append([f["face"] for f in isl["faces"]]) + else: + faces = [f for f in bm.faces] + islands = get_island_info_from_faces(bm, faces, uv_layer) + for isl in islands: + faces_list.append([f["face"] for f in isl["faces"]]) + elif method == 'FACE': + if only_selected: + for f in bm.faces: + if f.select: + faces_list.append([f]) + else: + for f in bm.faces: + faces_list.append([f]) + else: + raise ValueError("Invalid method: {}".format(method)) + + return faces_list + + +def measure_mesh_area(obj, calc_method, only_selected): bm = bmesh.from_edit_mesh(obj.data) if check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() - sel_faces = [f for f in bm.faces if f.select] + faces_list = get_faces_list(bm, calc_method, only_selected) - # measure + areas = [] + for faces in faces_list: + areas.append(measure_mesh_area_from_faces(faces)) + + return areas + + +def measure_mesh_area_from_faces(faces): mesh_area = 0.0 - for f in sel_faces: + for f in faces: verts = [l.vert.co for l in f.loops] f_mesh_area = calc_polygon_3d_area(verts) mesh_area = mesh_area + f_mesh_area @@ -405,7 +448,7 @@ def find_image(obj, face=None, tex_layer=None): if len(images) >= 2: raise RuntimeError("Find more than 2 images") - if len(images) == 0: + if not images: return None return images[0] @@ -428,40 +471,26 @@ def find_images(obj, face=None, tex_layer=None): return images -def measure_uv_area(obj, method='FIRST', tex_size=None): - bm = bmesh.from_edit_mesh(obj.data) - if check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - return None - uv_layer = bm.loops.layers.uv.verify() - - tex_layer = find_texture_layer(bm) - - sel_faces = [f for f in bm.faces if f.select] - - # measure +def measure_uv_area_from_faces(obj, faces, uv_layer, tex_layer, + tex_selection_method, tex_size): uv_area = 0.0 - for f in sel_faces: + for f in faces: uvs = [l[uv_layer].uv for l in f.loops] f_uv_area = calc_polygon_2d_area(uvs) # user specified - if method == 'USER_SPECIFIED' and tex_size is not None: + if tex_selection_method == 'USER_SPECIFIED' and tex_size is not None: img_size = tex_size # first texture if there are more than 2 textures assigned # to the object - elif method == 'FIRST': + elif tex_selection_method == 'FIRST': img = find_image(obj, f, tex_layer) # can not find from node, so we can not get texture size if not img: return None img_size = img.size # average texture size - elif method == 'AVERAGE': + elif tex_selection_method == 'AVERAGE': imgs = find_images(obj, f, tex_layer) if not imgs: return None @@ -473,7 +502,7 @@ def measure_uv_area(obj, method='FIRST', tex_size=None): img_size = [img_size_total[0] / len(imgs), img_size_total[1] / len(imgs)] # max texture size - elif method == 'MAX': + elif tex_selection_method == 'MAX': imgs = find_images(obj, f, tex_layer) if not imgs: return None @@ -484,7 +513,7 @@ def measure_uv_area(obj, method='FIRST', tex_size=None): max(img_size_max[1], img.size[1])] img_size = img_size_max # min texture size - elif method == 'MIN': + elif tex_selection_method == 'MIN': imgs = find_images(obj, f, tex_layer) if not imgs: return None @@ -495,13 +524,40 @@ def measure_uv_area(obj, method='FIRST', tex_size=None): min(img_size_min[1], img.size[1])] img_size = img_size_min else: - raise RuntimeError("Unexpected method: {}".format(method)) + raise RuntimeError("Unexpected method: {}" + .format(tex_selection_method)) - uv_area = uv_area + f_uv_area * img_size[0] * img_size[1] + uv_area += f_uv_area * img_size[0] * img_size[1] return uv_area +def measure_uv_area(obj, calc_method, tex_selection_method, tex_size, + only_selected): + bm = bmesh.from_edit_mesh(obj.data) + if check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + tex_layer = find_texture_layer(bm) + faces_list = get_faces_list(bm, calc_method, only_selected) + + # measure + uv_areas = [] + for faces in faces_list: + uv_area = measure_uv_area_from_faces( + obj, faces, uv_layer, tex_layer, tex_selection_method, tex_size) + if uv_area is None: + return None + uv_areas.append(uv_area) + + return uv_areas + + def diff_point_to_segment(a, b, p): ab = b - a normal_ab = ab.normalized() @@ -520,43 +576,42 @@ def diff_point_to_segment(a, b, p): # get selected loop pair whose loops are connected each other def __get_loop_pairs(l, uv_layer): - - def __get_loop_pairs_internal(l_, pairs_, uv_layer_, parsed_): - parsed_.append(l_) - for ll in l_.vert.link_loops: + pairs = [] + parsed = [] + loops_ready = [l] + while loops_ready: + l = loops_ready.pop(0) + parsed.append(l) + for ll in l.vert.link_loops: # forward direction lln = ll.link_loop_next # if there is same pair, skip it found = False - for p in pairs_: + for p in pairs: if (ll in p) and (lln in p): found = True break # two loops must be selected - if ll[uv_layer_].select and lln[uv_layer_].select: + if ll[uv_layer].select and lln[uv_layer].select: if not found: - pairs_.append([ll, lln]) - if lln not in parsed_: - __get_loop_pairs_internal(lln, pairs_, uv_layer_, parsed_) + pairs.append([ll, lln]) + if (lln not in parsed) and (lln not in loops_ready): + loops_ready.append(lln) # backward direction llp = ll.link_loop_prev # if there is same pair, skip it found = False - for p in pairs_: + for p in pairs: if (ll in p) and (llp in p): found = True break # two loops must be selected - if ll[uv_layer_].select and llp[uv_layer_].select: + if ll[uv_layer].select and llp[uv_layer].select: if not found: - pairs_.append([ll, llp]) - if llp not in parsed_: - __get_loop_pairs_internal(llp, pairs_, uv_layer_, parsed_) - - pairs = [] - parsed = [] - __get_loop_pairs_internal(l, pairs, uv_layer, parsed) + pairs.append([ll, llp]) + if (llp not in parsed) and (llp not in loops_ready): + loops_ready.append(llp) return pairs @@ -876,12 +931,12 @@ class RingBuffer: # clip: reference polygon # subject: tested polygon -def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode): +def __do_weiler_atherton_cliping(clip_uvs, subject_uvs, mode): - clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops]) + clip_uvs = RingBuffer(clip_uvs) if __is_polygon_flipped(clip_uvs): clip_uvs.reverse() - subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops]) + subject_uvs = RingBuffer(subject_uvs) if __is_polygon_flipped(subject_uvs): subject_uvs.reverse() @@ -1111,22 +1166,29 @@ def __is_points_in_polygon(points, subject_points): return True -def get_overlapped_uv_info(bm, faces, uv_layer, mode): +def get_overlapped_uv_info(bm_list, faces_list, uv_layer_list, mode): # at first, check island overlapped - isl = get_island_info_from_faces(bm, faces, uv_layer) + isl = [] + for bm, uv_layer, faces in zip(bm_list, uv_layer_list, faces_list): + info = get_island_info_from_faces(bm, faces, uv_layer) + isl.extend([(i, uv_layer) for i in info]) + overlapped_isl_pairs = [] - for i, i1 in enumerate(isl): - for i2 in isl[i + 1:]: + overlapped_uv_layer_pairs = [] + for i, (i1, uv_layer_1) in enumerate(isl): + for i2, uv_layer_2 in isl[i + 1:]: if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \ (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y): continue overlapped_isl_pairs.append([i1, i2]) + overlapped_uv_layer_pairs.append([uv_layer_1, uv_layer_2]) # next, check polygon overlapped overlapped_uvs = [] - for oip in overlapped_isl_pairs: + for oip, uvlp in zip(overlapped_isl_pairs, overlapped_uv_layer_pairs): for clip in oip[0]["faces"]: f_clip = clip["face"] + clip_uvs = [l[uvlp[0]].uv.copy() for l in f_clip.loops] for subject in oip[1]["faces"]: f_subject = subject["face"] @@ -1137,29 +1199,33 @@ def get_overlapped_uv_info(bm, faces, uv_layer, mode): (subject["max_uv"].y < clip["min_uv"].y): continue + subject_uvs = [l[uvlp[1]].uv.copy() for l in f_subject.loops] # slow operation, apply Weiler-Atherton cliping algorithm - result, polygons = __do_weiler_atherton_cliping(f_clip, - f_subject, - uv_layer, mode) + result, polygons = __do_weiler_atherton_cliping(clip_uvs, + subject_uvs, + mode) if result: - subject_uvs = [l[uv_layer].uv.copy() - for l in f_subject.loops] overlapped_uvs.append({"clip_face": f_clip, "subject_face": f_subject, + "clip_uv_layer": uvlp[0], + "subject_uv_layer": uvlp[1], "subject_uvs": subject_uvs, "polygons": polygons}) return overlapped_uvs -def get_flipped_uv_info(faces, uv_layer): +def get_flipped_uv_info(faces_list, uv_layer_list): flipped_uvs = [] - for f in faces: - polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops]) - if __is_polygon_flipped(polygon): - uvs = [l[uv_layer].uv.copy() for l in f.loops] - flipped_uvs.append({"face": f, "uvs": uvs, - "polygons": [polygon.as_list()]}) + for faces, uv_layer in zip(faces_list, uv_layer_list): + for f in faces: + polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops]) + if __is_polygon_flipped(polygon): + uvs = [l[uv_layer].uv.copy() for l in f.loops] + flipped_uvs.append({"face": f, + "uv_layer": uv_layer, + "uvs": uvs, + "polygons": [polygon.as_list()]}) return flipped_uvs diff --git a/magic_uv/lib/__init__.py b/magic_uv/lib/__init__.py index 3258b6eb..5e06552d 100644 --- a/magic_uv/lib/__init__.py +++ b/magic_uv/lib/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" if "bpy" in locals(): import importlib diff --git a/magic_uv/op/__init__.py b/magic_uv/op/__init__.py index cd743b48..b7316192 100644 --- a/magic_uv/op/__init__.py +++ b/magic_uv/op/__init__.py @@ -20,13 +20,14 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" if "bpy" in locals(): import importlib importlib.reload(align_uv) importlib.reload(align_uv_cursor) + importlib.reload(clip_uv) importlib.reload(copy_paste_uv) importlib.reload(copy_paste_uv_object) importlib.reload(copy_paste_uv_uvedit) @@ -50,6 +51,7 @@ if "bpy" in locals(): else: from . import align_uv from . import align_uv_cursor + from . import clip_uv from . import copy_paste_uv from . import copy_paste_uv_object from . import copy_paste_uv_uvedit diff --git a/magic_uv/op/align_uv.py b/magic_uv/op/align_uv.py index 31f7cbe8..77afcc25 100644 --- a/magic_uv/op/align_uv.py +++ b/magic_uv/op/align_uv.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import math from math import atan2, tan, sin, cos @@ -164,8 +164,7 @@ def _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): # calculate target UV for i in range(len(accum_uvlens[:-1])): # get line segment which UV will be placed - if ((accum_uvlens[i] <= target_length) and - (accum_uvlens[i + 1] > target_length)): + if accum_uvlens[i] <= target_length < accum_uvlens[i + 1]: tgt_seg_len = target_length - accum_uvlens[i] seg_len = accum_uvlens[i + 1] - accum_uvlens[i] uv1 = orig_uvs[i] @@ -245,8 +244,7 @@ def _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): # calculate target UV for i in range(len(accum_uvlens[:-1])): # get line segment which UV will be placed - if ((accum_uvlens[i] <= target_length) and - (accum_uvlens[i + 1] > target_length)): + if accum_uvlens[i] <= target_length < accum_uvlens[i + 1]: tgt_seg_len = target_length - accum_uvlens[i] seg_len = accum_uvlens[i + 1] - accum_uvlens[i] uv1 = orig_uvs[i] diff --git a/magic_uv/op/align_uv_cursor.py b/magic_uv/op/align_uv_cursor.py index b103de31..884f645a 100644 --- a/magic_uv/op/align_uv_cursor.py +++ b/magic_uv/op/align_uv_cursor.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from mathutils import Vector diff --git a/magic_uv/op/clip_uv.py b/magic_uv/op/clip_uv.py new file mode 100644 index 00000000..c6f006e2 --- /dev/null +++ b/magic_uv/op/clip_uv.py @@ -0,0 +1,227 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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 2 +# 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 +# MERCHANTABILITY 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, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Dusan Stevanovic, Nutti " +__status__ = "production" +__version__ = "6.3" +__date__ = "10 Aug 2020" + + +import math + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import BoolProperty, FloatVectorProperty + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +def round_clip_uv_range(v): + sign = 1 if v >= 0.0 else -1 + return int((math.fabs(v) + 0.25) / 0.5) * 0.5 * sign + + +def get_clip_uv_range_max(self): + return self.get('muv_clip_uv_range_max', (0.5, 0.5)) + + +def set_clip_uv_range_max(self, value): + u = round_clip_uv_range(value[0]) + u = 0.5 if u <= 0.5 else u + v = round_clip_uv_range(value[1]) + v = 0.5 if v <= 0.5 else v + self['muv_clip_uv_range_max'] = (u, v) + + +def get_clip_uv_range_min(self): + return self.get('muv_clip_uv_range_min', (-0.5, -0.5)) + + +def set_clip_uv_range_min(self, value): + u = round_clip_uv_range(value[0]) + u = -0.5 if u >= -0.5 else u + v = round_clip_uv_range(value[1]) + v = -0.5 if v >= -0.5 else v + self['muv_clip_uv_range_min'] = (u, v) + + +@PropertyClassRegistry() +class _Properties: + idname = "clip_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_clip_uv_enabled = BoolProperty( + name="Clip UV Enabled", + description="Clip UV is enabled", + default=False + ) + + scene.muv_clip_uv_range_max = FloatVectorProperty( + name="Range Max", + description="Max UV coordinates of the range to be clipped", + size=2, + default=(0.5, 0.5), + min=0.5, + step=50, + get=get_clip_uv_range_max, + set=set_clip_uv_range_max, + ) + + scene.muv_clip_uv_range_min = FloatVectorProperty( + name="Range Min", + description="Min UV coordinates of the range to be clipped", + size=2, + default=(-0.5, -0.5), + max=-0.5, + step=50, + get=get_clip_uv_range_min, + set=set_clip_uv_range_min, + ) + + # TODO: add option to preserve UV island + + @classmethod + def del_props(cls, scene): + del scene.muv_clip_uv_range_max + del scene.muv_clip_uv_range_min + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_ClipUV(bpy.types.Operator): + + bl_idname = "uv.muv_clip_uv" + bl_label = "Clip UV" + bl_description = "Clip selected UV in the specified range" + bl_options = {'REGISTER', 'UNDO'} + + clip_uv_range_max = FloatVectorProperty( + name="Range Max", + description="Max UV coordinates of the range to be clipped", + size=2, + default=(0.5, 0.5), + min=0.5, + step=50, + ) + + clip_uv_range_min = FloatVectorProperty( + name="Range Min", + description="Min UV coordinates of the range to be clipped", + size=2, + default=(-0.5, -0.5), + max=-0.5, + step=50, + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + obj = context.active_object + bm = common.create_bmesh(obj) + + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + + uv_layer = bm.loops.layers.uv.verify() + + for face in bm.faces: + if not face.select: + continue + + selected_loops = [l for l in face.loops + if l[uv_layer].select or + context.scene.tool_settings.use_uv_select_sync] + if not selected_loops: + continue + + # average of UV coordinates on the face + max_uv = Vector((-10000000.0, -10000000.0)) + min_uv = Vector((10000000.0, 10000000.0)) + for l in selected_loops: + uv = l[uv_layer].uv + max_uv.x = max(max_uv.x, uv.x) + max_uv.y = max(max_uv.y, uv.y) + min_uv.x = min(min_uv.x, uv.x) + min_uv.y = min(min_uv.y, uv.y) + + # clip + move_uv = Vector((0.0, 0.0)) + clip_size = Vector(self.clip_uv_range_max) - \ + Vector(self.clip_uv_range_min) + if max_uv.x > self.clip_uv_range_max[0]: + target_x = math.fmod(max_uv.x - self.clip_uv_range_min[0], + clip_size.x) + if target_x < 0.0: + target_x += clip_size.x + target_x += self.clip_uv_range_min[0] + move_uv.x = target_x - max_uv.x + if min_uv.x < self.clip_uv_range_min[0]: + target_x = math.fmod(min_uv.x - self.clip_uv_range_min[0], + clip_size.x) + if target_x < 0.0: + target_x += clip_size.x + target_x += self.clip_uv_range_min[0] + move_uv.x = target_x - min_uv.x + if max_uv.y > self.clip_uv_range_max[1]: + target_y = math.fmod(max_uv.y - self.clip_uv_range_min[1], + clip_size.y) + if target_y < 0.0: + target_y += clip_size.y + target_y += self.clip_uv_range_min[1] + move_uv.y = target_y - max_uv.y + if min_uv.y < self.clip_uv_range_min[1]: + target_y = math.fmod(min_uv.y - self.clip_uv_range_min[1], + clip_size.y) + if target_y < 0.0: + target_y += clip_size.y + target_y += self.clip_uv_range_min[1] + move_uv.y = target_y - min_uv.y + + # update UV + for l in selected_loops: + l[uv_layer].uv = l[uv_layer].uv + move_uv + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/copy_paste_uv.py b/magic_uv/op/copy_paste_uv.py index 5126e241..ba754425 100644 --- a/magic_uv/op/copy_paste_uv.py +++ b/magic_uv/op/copy_paste_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti , Jace Priester" __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bmesh import bpy.utils @@ -75,7 +75,7 @@ def get_copy_uv_layers(ops_obj, bm, uv_map): else: uv_layers.append(bm.loops.layers.uv[uv_map]) ops_obj.report( - {'INFO'}, "Copy UV coordinate (UV map:{})".format(uv_map)) + {'INFO'}, "Copy UV coordinate (UV map: {})".format(uv_map)) return uv_layers @@ -97,7 +97,8 @@ def get_paste_uv_layers(ops_obj, obj, bm, src_info, uv_map): return None uv_layers.append(bm.loops.layers.uv[new_uv_map.name]) ops_obj.report( - {'INFO'}, "Paste UV coordinate (UV map:{})".format(new_uv_map)) + {'INFO'}, + "Paste UV coordinate (UV map: {})".format(new_uv_map.name)) elif uv_map == "__all": for src_layer in src_info.keys(): if src_layer not in bm.loops.layers.uv.keys(): @@ -111,7 +112,7 @@ def get_paste_uv_layers(ops_obj, obj, bm, src_info, uv_map): else: uv_layers.append(bm.loops.layers.uv[uv_map]) ops_obj.report( - {'INFO'}, "Paste UV coordinate (UV map:{})".format(uv_map)) + {'INFO'}, "Paste UV coordinate (UV map: {})".format(uv_map)) return uv_layers diff --git a/magic_uv/op/copy_paste_uv_object.py b/magic_uv/op/copy_paste_uv_object.py index 3297f2b8..1b812b82 100644 --- a/magic_uv/op/copy_paste_uv_object.py +++ b/magic_uv/op/copy_paste_uv_object.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bmesh import bpy diff --git a/magic_uv/op/copy_paste_uv_uvedit.py b/magic_uv/op/copy_paste_uv_uvedit.py index 7704d1c9..f12851dd 100644 --- a/magic_uv/op/copy_paste_uv_uvedit.py +++ b/magic_uv/op/copy_paste_uv_uvedit.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import math from math import atan2, sin, cos diff --git a/magic_uv/op/flip_rotate_uv.py b/magic_uv/op/flip_rotate_uv.py index da8af4c3..d0ac6a83 100644 --- a/magic_uv/op/flip_rotate_uv.py +++ b/magic_uv/op/flip_rotate_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy import bmesh diff --git a/magic_uv/op/mirror_uv.py b/magic_uv/op/mirror_uv.py index d28cf826..dcbaad5e 100644 --- a/magic_uv/op/mirror_uv.py +++ b/magic_uv/op/mirror_uv.py @@ -20,8 +20,8 @@ __author__ = "Keith (Wahooney) Boshoff, Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import ( diff --git a/magic_uv/op/move_uv.py b/magic_uv/op/move_uv.py index 881ab378..19160a46 100644 --- a/magic_uv/op/move_uv.py +++ b/magic_uv/op/move_uv.py @@ -20,8 +20,8 @@ __author__ = "kgeogeo, mem, Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import BoolProperty @@ -54,20 +54,6 @@ def _is_valid_context(context): return True -def _find_uv(context): - bm = bmesh.from_edit_mesh(context.object.data) - topology_dict = [] - uvs = [] - active_uv = bm.loops.layers.uv.active - for fidx, f in enumerate(bm.faces): - for vidx, v in enumerate(f.verts): - if v.select: - uvs.append(f.loops[vidx][active_uv].uv.copy()) - topology_dict.append([fidx, vidx]) - - return topology_dict, uvs - - @PropertyClassRegistry() class _Properties: idname = "move_uv" @@ -106,6 +92,9 @@ class MUV_OT_MoveUV(bpy.types.Operator): self.__ini_uvs = [] self.__operating = False + # Creation of BMesh is high cost, so cache related objects. + self.__cache = {} + @classmethod def poll(cls, context): # we can not get area/space/region from console @@ -119,7 +108,18 @@ class MUV_OT_MoveUV(bpy.types.Operator): def is_running(cls, _): return cls.__running - def modal(self, context, event): + def _find_uv(self, bm, active_uv): + topology_dict = [] + uvs = [] + for fidx, f in enumerate(bm.faces): + for vidx, v in enumerate(f.verts): + if v.select: + uvs.append(f.loops[vidx][active_uv].uv.copy()) + topology_dict.append([fidx, vidx]) + + return topology_dict, uvs + + def modal(self, _, event): if self.__first_time is True: self.__prev_mouse = Vector(( event.mouse_region_x, event.mouse_region_y)) @@ -146,12 +146,11 @@ class MUV_OT_MoveUV(bpy.types.Operator): return {'RUNNING_MODAL'} # update UV - obj = context.object - bm = bmesh.from_edit_mesh(obj.data) - active_uv = bm.loops.layers.uv.active - for fidx, vidx in self.__topology_dict: - l = bm.faces[fidx].loops[vidx] - l[active_uv].uv = l[active_uv].uv + dv + obj = self.__cache["active_object"] + bm = self.__cache["bmesh"] + active_uv = self.__cache["active_uv"] + for uv in self.__cache["target_uv"]: + uv += dv bmesh.update_edit_mesh(obj.data) # check mouse preference @@ -163,10 +162,12 @@ class MUV_OT_MoveUV(bpy.types.Operator): for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): bm.faces[fidx].loops[vidx][active_uv].uv = uv MUV_OT_MoveUV.__running = False + self.__cache = {} return {'FINISHED'} # confirmed if event.type == confirm_btn and event.value == 'PRESS': MUV_OT_MoveUV.__running = False + self.__cache = {} return {'FINISHED'} return {'RUNNING_MODAL'} @@ -177,7 +178,21 @@ class MUV_OT_MoveUV(bpy.types.Operator): self.__first_time = True context.window_manager.modal_handler_add(self) - self.__topology_dict, self.__ini_uvs = _find_uv(context) + + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + active_uv = bm.loops.layers.uv.active + self.__topology_dict, self.__ini_uvs = self._find_uv(bm, active_uv) + + # Optimization: Store temporary variables which cause heavy + # calculation. + self.__cache["active_object"] = obj + self.__cache["bmesh"] = bm + self.__cache["active_uv"] = active_uv + self.__cache["target_uv"] = [] + for fidx, vidx in self.__topology_dict: + l = bm.faces[fidx].loops[vidx] + self.__cache["target_uv"].append(l[active_uv].uv) if context.area: context.area.tag_redraw() diff --git a/magic_uv/op/pack_uv.py b/magic_uv/op/pack_uv.py index 3589231a..0d7ed966 100644 --- a/magic_uv/op/pack_uv.py +++ b/magic_uv/op/pack_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from math import fabs diff --git a/magic_uv/op/preserve_uv_aspect.py b/magic_uv/op/preserve_uv_aspect.py index c9ba7204..5b3e50cf 100644 --- a/magic_uv/op/preserve_uv_aspect.py +++ b/magic_uv/op/preserve_uv_aspect.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import StringProperty, EnumProperty, BoolProperty diff --git a/magic_uv/op/select_uv.py b/magic_uv/op/select_uv.py index 223f9e2f..d80b43a8 100644 --- a/magic_uv/op/select_uv.py +++ b/magic_uv/op/select_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import BoolProperty @@ -30,6 +30,7 @@ import bmesh from .. import common from ..utils.bl_class_registry import BlClassRegistry from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat def _is_valid_context(context): @@ -91,28 +92,42 @@ class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() + objs = [o for o in bpy.data.objects if compat.get_object_select(o)] + + bm_list = [] + uv_layer_list = [] + faces_list = [] + for o in bpy.data.objects: + if not compat.get_object_select(o): + continue + if o.type != 'MESH': + continue + + bm = bmesh.from_edit_mesh(o.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() - if context.tool_settings.use_uv_select_sync: - sel_faces = [f for f in bm.faces] - else: - sel_faces = [f for f in bm.faces if f.select] + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + bm_list.append(bm) + uv_layer_list.append(uv_layer) + faces_list.append(sel_faces) - overlapped_info = common.get_overlapped_uv_info(bm, sel_faces, - uv_layer, 'FACE') + overlapped_info = common.get_overlapped_uv_info(bm_list, faces_list, + uv_layer_list, 'FACE') for info in overlapped_info: if context.tool_settings.use_uv_select_sync: info["subject_face"].select = True else: for l in info["subject_face"].loops: - l[uv_layer].select = True + l[info["subject_uv_layer"]].select = True - bmesh.update_edit_mesh(obj.data) + for o in objs: + bmesh.update_edit_mesh(o.data) return {'FINISHED'} @@ -136,26 +151,40 @@ class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() + objs = [o for o in bpy.data.objects if compat.get_object_select(o)] + + bm_list = [] + uv_layer_list = [] + faces_list = [] + for o in bpy.data.objects: + if not compat.get_object_select(o): + continue + if o.type != 'MESH': + continue + + bm = bmesh.from_edit_mesh(o.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() - if context.tool_settings.use_uv_select_sync: - sel_faces = [f for f in bm.faces] - else: - sel_faces = [f for f in bm.faces if f.select] + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + bm_list.append(bm) + uv_layer_list.append(uv_layer) + faces_list.append(sel_faces) - flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer) + flipped_info = common.get_flipped_uv_info(faces_list, uv_layer_list) for info in flipped_info: if context.tool_settings.use_uv_select_sync: info["face"].select = True else: for l in info["face"].loops: - l[uv_layer].select = True + l[info["uv_layer"]].select = True - bmesh.update_edit_mesh(obj.data) + for o in objs: + bmesh.update_edit_mesh(o.data) return {'FINISHED'} diff --git a/magic_uv/op/smooth_uv.py b/magic_uv/op/smooth_uv.py index 17068308..94e41367 100644 --- a/magic_uv/op/smooth_uv.py +++ b/magic_uv/op/smooth_uv.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import BoolProperty, FloatProperty @@ -167,8 +167,7 @@ class MUV_OT_SmoothUV(bpy.types.Operator): # get target UV for i in range(len(accm_uvlens[:-1])): # get line segment which UV will be placed - if ((accm_uvlens[i] <= target_length) and - (accm_uvlens[i + 1] > target_length)): + if accm_uvlens[i] <= target_length < accm_uvlens[i + 1]: tgt_seg_len = target_length - accm_uvlens[i] seg_len = accm_uvlens[i + 1] - accm_uvlens[i] uv1 = orig_uvs[i] @@ -240,8 +239,7 @@ class MUV_OT_SmoothUV(bpy.types.Operator): # get target UV for i in range(len(accm_uv[:-1])): # get line segment to be placed - if ((accm_uv[i] <= target_length) and - (accm_uv[i + 1] > target_length)): + if accm_uv[i] <= target_length < accm_uv[i + 1]: tgt_seg_len = target_length - accm_uv[i] seg_len = accm_uv[i + 1] - accm_uv[i] uv1 = uvs[i] diff --git a/magic_uv/op/texture_lock.py b/magic_uv/op/texture_lock.py index 43d78549..ddcaf315 100644 --- a/magic_uv/op/texture_lock.py +++ b/magic_uv/op/texture_lock.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import math from math import atan2, cos, sqrt, sin, fabs @@ -435,7 +435,7 @@ class MUV_OT_TextureLock_Intr(bpy.types.Operator): bm.faces.ensure_lookup_table() prev = set(self.__intr_verts) - now = set([v.index for v in bm.verts if v.select]) + now = {v.index for v in bm.verts if v.select} return prev != now diff --git a/magic_uv/op/texture_projection.py b/magic_uv/op/texture_projection.py index 6ef6b1ce..b754dd88 100644 --- a/magic_uv/op/texture_projection.py +++ b/magic_uv/op/texture_projection.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from collections import namedtuple diff --git a/magic_uv/op/texture_wrap.py b/magic_uv/op/texture_wrap.py index 9936a510..92512438 100644 --- a/magic_uv/op/texture_wrap.py +++ b/magic_uv/op/texture_wrap.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import ( diff --git a/magic_uv/op/transfer_uv.py b/magic_uv/op/transfer_uv.py index b63376c9..ce9639a7 100644 --- a/magic_uv/op/transfer_uv.py +++ b/magic_uv/op/transfer_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti , Mifth, MaxRobinot" __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from collections import OrderedDict diff --git a/magic_uv/op/unwrap_constraint.py b/magic_uv/op/unwrap_constraint.py index bd78dafc..3c23575a 100644 --- a/magic_uv/op/unwrap_constraint.py +++ b/magic_uv/op/unwrap_constraint.py @@ -18,8 +18,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import ( diff --git a/magic_uv/op/uv_bounding_box.py b/magic_uv/op/uv_bounding_box.py index 589abcc4..d4edac9c 100644 --- a/magic_uv/op/uv_bounding_box.py +++ b/magic_uv/op/uv_bounding_box.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from enum import IntEnum import math @@ -438,10 +438,8 @@ class StateNone(StateBase): mouse_view.x, mouse_view.y) for i, p in enumerate(ctrl_points): px, py = context.region.view2d.view_to_region(p.x, p.y) - in_cp_x = (px + cp_react_size > x and - px - cp_react_size < x) - in_cp_y = (py + cp_react_size > y and - py - cp_react_size < y) + in_cp_x = px - cp_react_size < x < px + cp_react_size + in_cp_y = py - cp_react_size < y < py + cp_react_size if in_cp_x and in_cp_y: if is_uscaling: arr = [1, 3, 6, 8] diff --git a/magic_uv/op/uv_inspection.py b/magic_uv/op/uv_inspection.py index c5f92004..8aae181e 100644 --- a/magic_uv/op/uv_inspection.py +++ b/magic_uv/op/uv_inspection.py @@ -20,8 +20,11 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" + +import random +from math import fabs import bpy from bpy.props import BoolProperty, EnumProperty @@ -65,19 +68,31 @@ def _update_uvinsp_info(context): sc = context.scene props = sc.muv_props.uv_inspection - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() + bm_list = [] + uv_layer_list = [] + faces_list = [] + for o in bpy.data.objects: + if not compat.get_object_select(o): + continue + if o.type != 'MESH': + continue + + bm = bmesh.from_edit_mesh(o.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + bm_list.append(bm) + uv_layer_list.append(uv_layer) + faces_list.append(sel_faces) - if context.tool_settings.use_uv_select_sync: - sel_faces = [f for f in bm.faces] - else: - sel_faces = [f for f in bm.faces if f.select] props.overlapped_info = common.get_overlapped_uv_info( - bm, sel_faces, uv_layer, sc.muv_uv_inspection_show_mode) - props.flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer) + bm_list, faces_list, uv_layer_list, sc.muv_uv_inspection_show_mode) + props.flipped_info = common.get_flipped_uv_info(faces_list, uv_layer_list) @PropertyClassRegistry() @@ -205,14 +220,15 @@ class MUV_OT_UVInspection_Render(bpy.types.Operator): bgl.glColor4f(color[0], color[1], color[2], color[3]) for uv in poly: x, y = context.region.view2d.view_to_region( - uv.x, uv.y) + uv.x, uv.y, clip=False) bgl.glVertex2f(x, y) bgl.glEnd() elif sc.muv_uv_inspection_show_mode == 'FACE': bgl.glBegin(bgl.GL_TRIANGLE_FAN) bgl.glColor4f(color[0], color[1], color[2], color[3]) for uv in info["subject_uvs"]: - x, y = context.region.view2d.view_to_region(uv.x, uv.y) + x, y = context.region.view2d.view_to_region( + uv.x, uv.y, clip=False) bgl.glVertex2f(x, y) bgl.glEnd() @@ -226,14 +242,15 @@ class MUV_OT_UVInspection_Render(bpy.types.Operator): bgl.glColor4f(color[0], color[1], color[2], color[3]) for uv in poly: x, y = context.region.view2d.view_to_region( - uv.x, uv.y) + uv.x, uv.y, clip=False) bgl.glVertex2f(x, y) bgl.glEnd() elif sc.muv_uv_inspection_show_mode == 'FACE': bgl.glBegin(bgl.GL_TRIANGLE_FAN) bgl.glColor4f(color[0], color[1], color[2], color[3]) for uv in info["uvs"]: - x, y = context.region.view2d.view_to_region(uv.x, uv.y) + x, y = context.region.view2d.view_to_region( + uv.x, uv.y, clip=False) bgl.glVertex2f(x, y) bgl.glEnd() @@ -279,3 +296,201 @@ class MUV_OT_UVInspection_Update(bpy.types.Operator): context.area.tag_redraw() return {'FINISHED'} + + +@BlClassRegistry() +class MUV_OT_UVInspection_PaintUVIsland(bpy.types.Operator): + """ + Operation class: Paint UV island with random color. + """ + + bl_idname = "uv.muv_uv_inspection_paint_uv_island" + bl_label = "Paint UV Island" + bl_description = "Paint UV island with random color" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def _get_or_new_image(self, name, width, height): + if name in bpy.data.images.keys(): + return bpy.data.images[name] + return bpy.data.images.new(name, width, height) + + def _get_or_new_material(self, name): + if name in bpy.data.materials.keys(): + return bpy.data.materials[name] + return bpy.data.materials.new(name) + + def _get_or_new_texture(self, name): + if name in bpy.data.textures.keys(): + return bpy.data.textures[name] + return bpy.data.textures.new(name, 'IMAGE') + + def _get_override_context(self, context): + for window in context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + return {'window': window, 'screen': screen, + 'area': area, 'region': region} + return None + + def _create_unique_color(self, exist_colors, allowable=0.1): + retry = 0 + while retry < 20: + r = random.random() + g = random.random() + b = random.random() + new_color = [r, g, b] + for color in exist_colors: + if ((fabs(new_color[0] - color[0]) < allowable) and + (fabs(new_color[1] - color[1]) < allowable) and + (fabs(new_color[2] - color[2]) < allowable)): + break + else: + return new_color + return None + + def execute(self, context): + obj = context.active_object + mode_orig = context.object.mode + override_context = self._get_override_context(context) + if override_context is None: + self.report({'WARNING'}, "More than one 'VIEW_3D' area must exist") + return {'CANCELLED'} + + # Setup material of drawing target. + target_image = self._get_or_new_image( + "MagicUV_PaintUVIsland", 4096, 4096) + target_mtrl = self._get_or_new_material("MagicUV_PaintUVMaterial") + if compat.check_version(2, 80, 0) >= 0: + target_mtrl.use_nodes = True + output_node = target_mtrl.node_tree.nodes["Material Output"] + nodes_to_remove = [n for n in target_mtrl.node_tree.nodes + if n != output_node] + for n in nodes_to_remove: + target_mtrl.node_tree.nodes.remove(n) + texture_node = \ + target_mtrl.node_tree.nodes.new("ShaderNodeTexImage") + texture_node.image = target_image + target_mtrl.node_tree.links.new(output_node.inputs["Surface"], + texture_node.outputs["Color"]) + obj.data.use_paint_mask = True + + # Apply material to object (all faces). + found = False + for mtrl_idx, mtrl_slot in enumerate(obj.material_slots): + if mtrl_slot.material == target_mtrl: + found = True + break + if not found: + bpy.ops.object.material_slot_add() + mtrl_idx = len(obj.material_slots) - 1 + obj.material_slots[mtrl_idx].material = target_mtrl + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + for f in bm.faces: + f.select = True + bmesh.update_edit_mesh(obj.data) + obj.active_material_index = mtrl_idx + obj.active_material = target_mtrl + bpy.ops.object.material_slot_assign() + else: + target_tex_slot = target_mtrl.texture_slots.add() + target_tex = self._get_or_new_texture("MagicUV_PaintUVTexture") + target_tex_slot.texture = target_tex + obj.data.use_paint_mask = True + + # Apply material to object (all faces). + found = False + for mtrl_idx, mtrl_slot in enumerate(obj.material_slots): + if mtrl_slot.material == target_mtrl: + found = True + break + if not found: + bpy.ops.object.material_slot_add() + mtrl_idx = len(obj.material_slots) - 1 + obj.material_slots[mtrl_idx].material = target_mtrl + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + for f in bm.faces: + f.select = True + bmesh.update_edit_mesh(obj.data) + obj.active_material_index = mtrl_idx + obj.active_material = target_mtrl + bpy.ops.object.material_slot_assign() + + # Update active image in Image Editor. + _, _, space = common.get_space( + 'IMAGE_EDITOR', 'WINDOW', 'IMAGE_EDITOR') + if space is None: + return {'CANCELLED'} + space.image = target_image + + # Analyze island to make map between face and paint color. + islands = common.get_island_info_from_bmesh(bm) + color_to_faces = [] + for isl in islands: + color = self._create_unique_color([c[0] for c in color_to_faces]) + if color is None: + self.report({'WARNING'}, + "Failed to create color. Please try again") + return {'CANCELLED'} + indices = [f["face"].index for f in isl["faces"]] + color_to_faces.append((color, indices)) + + for cf in color_to_faces: + # Update selection information. + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + for f in bm.faces: + f.select = False + for fidx in cf[1]: + bm.faces[fidx].select = True + bmesh.update_edit_mesh(obj.data) + bpy.ops.object.mode_set(mode='OBJECT') + + # Update brush color. + bpy.data.brushes["Fill"].color = cf[0] + + # Paint. + bpy.ops.object.mode_set(mode='TEXTURE_PAINT') + if compat.check_version(2, 80, 0) >= 0: + bpy.ops.paint.brush_select(override_context, image_tool='FILL') + else: + paint_settings = \ + bpy.data.scenes['Scene'].tool_settings.image_paint + paint_mode_orig = paint_settings.mode + paint_canvas_orig = paint_settings.canvas + paint_settings.mode = 'IMAGE' + paint_settings.canvas = target_image + bpy.ops.paint.brush_select(override_context, + texture_paint_tool='FILL') + bpy.ops.paint.image_paint(override_context, stroke=[{ + "name": "", + "location": (0, 0, 0), + "mouse": (0, 0), + "size": 0, + "pressure": 0, + "pen_flip": False, + "time": 0, + "is_start": False + }]) + + if compat.check_version(2, 80, 0) < 0: + paint_settings.mode = paint_mode_orig + paint_settings.canvas = paint_canvas_orig + + bpy.ops.object.mode_set(mode=mode_orig) + + return {'FINISHED'} diff --git a/magic_uv/op/uv_sculpt.py b/magic_uv/op/uv_sculpt.py index ff3a9db3..f40ab253 100644 --- a/magic_uv/op/uv_sculpt.py +++ b/magic_uv/op/uv_sculpt.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from math import pi, cos, tan, sin @@ -168,6 +168,20 @@ class _Properties: del scene.muv_uv_sculpt_relax_method +def location_3d_to_region_2d_extra(region, rv3d, coord): + coord_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, coord) + if coord_2d is None: + prj = rv3d.perspective_matrix @ Vector( + (coord[0], coord[1], coord[2], 1.0)) + width_half = region.width / 2.0 + height_half = region.height / 2.0 + coord_2d = Vector(( + width_half + width_half * (prj.x / prj.w), + height_half + height_half * (prj.y / prj.w) + )) + return coord_2d + + @BlClassRegistry() class MUV_OT_UVSculpt(bpy.types.Operator): """ @@ -263,7 +277,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator): if not f.select: continue for i, l in enumerate(f.loops): - loc_2d = view3d_utils.location_3d_to_region_2d( + loc_2d = location_3d_to_region_2d_extra( region, space.region_3d, compat.matmul(world_mat, l.vert.co)) diff = loc_2d - self.__initial_mco @@ -301,7 +315,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator): if not f.select: continue for i, l in enumerate(f.loops): - loc_2d = view3d_utils.location_3d_to_region_2d( + loc_2d = location_3d_to_region_2d_extra( region, space.region_3d, compat.matmul(world_mat, l.vert.co)) diff = loc_2d - self.__initial_mco @@ -393,7 +407,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator): if not f.select: continue for i, l in enumerate(f.loops): - loc_2d = view3d_utils.location_3d_to_region_2d( + loc_2d = location_3d_to_region_2d_extra( region, space.region_3d, compat.matmul(world_mat, l.vert.co)) diff = loc_2d - self.__initial_mco diff --git a/magic_uv/op/uvw.py b/magic_uv/op/uvw.py index 4b4a4f04..fca72d2c 100644 --- a/magic_uv/op/uvw.py +++ b/magic_uv/op/uvw.py @@ -20,8 +20,8 @@ __author__ = "Alexander Milovsky, Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from math import sin, cos, pi @@ -228,20 +228,26 @@ class MUV_OT_UVW_BoxMap(bpy.types.Operator): return True return _is_valid_context(context) - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + def execute(self, _): + if compat.check_version(2, 80, 0) < 0: + objs = [bpy.context.active_object] + else: + objs = [o for o in bpy.data.objects + if compat.get_object_select(o) and o.type == 'MESH'] + + for o in objs: + bm = bmesh.from_edit_mesh(o.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() - # get UV layer - uv_layer = _get_uv_layer(self, bm, self.assign_uvmap) - if not uv_layer: - return {'CANCELLED'} + # get UV layer + uv_layer = _get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} - _apply_box_map(bm, uv_layer, self.size, self.offset, self.rotation, - self.tex_aspect) - bmesh.update_edit_mesh(obj.data) + _apply_box_map(bm, uv_layer, self.size, self.offset, self.rotation, + self.tex_aspect) + bmesh.update_edit_mesh(o.data) return {'FINISHED'} @@ -285,20 +291,26 @@ class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator): return True return _is_valid_context(context) - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + def execute(self, _): + if compat.check_version(2, 80, 0) < 0: + objs = [bpy.context.active_object] + else: + objs = [o for o in bpy.data.objects + if compat.get_object_select(o) and o.type == 'MESH'] + + for o in objs: + bm = bmesh.from_edit_mesh(o.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() - # get UV layer - uv_layer = _get_uv_layer(self, bm, self.assign_uvmap) - if not uv_layer: - return {'CANCELLED'} + # get UV layer + uv_layer = _get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} - _apply_planer_map(bm, uv_layer, self.size, self.offset, self.rotation, - self.tex_aspect) + _apply_planer_map(bm, uv_layer, self.size, self.offset, + self.rotation, self.tex_aspect) - bmesh.update_edit_mesh(obj.data) + bmesh.update_edit_mesh(o.data) return {'FINISHED'} diff --git a/magic_uv/op/world_scale_uv.py b/magic_uv/op/world_scale_uv.py index 0107fc6f..9ed86eb0 100644 --- a/magic_uv/op/world_scale_uv.py +++ b/magic_uv/op/world_scale_uv.py @@ -20,8 +20,8 @@ __author__ = "McBuff, Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from math import sqrt @@ -31,7 +31,6 @@ from bpy.props import ( FloatProperty, IntVectorProperty, BoolProperty, - StringProperty, ) import bmesh from mathutils import Vector @@ -63,9 +62,34 @@ def _is_valid_context(context): return True -def _measure_wsuv_info(obj, method='FIRST', tex_size=None): - mesh_area = common.measure_mesh_area(obj) - uv_area = common.measure_uv_area(obj, method, tex_size) +def _measure_wsuv_info(obj, calc_method='MESH', + tex_selection_method='FIRST', tex_size=None, + only_selected=True): + mesh_areas = common.measure_mesh_area(obj, calc_method, only_selected) + uv_areas = common.measure_uv_area(obj, calc_method, tex_selection_method, + tex_size, only_selected) + + if not uv_areas: + return None, mesh_areas, None + + if len(mesh_areas) != len(uv_areas): + raise ValueError("mesh_area and uv_area must be same length") + + densities = [] + for mesh_area, uv_area in zip(mesh_areas, uv_areas): + if mesh_area == 0.0: + densities.append(0.0) + else: + densities.append(sqrt(uv_area) / sqrt(mesh_area)) + + return uv_areas, mesh_areas, densities + + +def _measure_wsuv_info_from_faces(obj, faces, uv_layer, tex_layer, + tex_selection_method='FIRST', tex_size=None): + mesh_area = common.measure_mesh_area_from_faces(faces) + uv_area = common.measure_uv_area_from_faces( + obj, faces, uv_layer, tex_layer, tex_selection_method, tex_size) if not uv_area: return None, mesh_area, None @@ -78,22 +102,12 @@ def _measure_wsuv_info(obj, method='FIRST', tex_size=None): return uv_area, mesh_area, density -def _apply(obj, origin, factor): - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - sel_faces = [f for f in bm.faces if f.select] - - uv_layer = bm.loops.layers.uv.verify() - +def _apply(faces, uv_layer, origin, factor): # calculate origin if origin == 'CENTER': origin = Vector((0.0, 0.0)) num = 0 - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin = origin + uv @@ -101,7 +115,7 @@ def _apply(obj, origin, factor): origin = origin / num elif origin == 'LEFT_TOP': origin = Vector((100000.0, -100000.0)) - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = min(origin.x, uv.x) @@ -109,7 +123,7 @@ def _apply(obj, origin, factor): elif origin == 'LEFT_CENTER': origin = Vector((100000.0, 0.0)) num = 0 - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = min(origin.x, uv.x) @@ -118,7 +132,7 @@ def _apply(obj, origin, factor): origin.y = origin.y / num elif origin == 'LEFT_BOTTOM': origin = Vector((100000.0, 100000.0)) - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = min(origin.x, uv.x) @@ -126,7 +140,7 @@ def _apply(obj, origin, factor): elif origin == 'CENTER_TOP': origin = Vector((0.0, -100000.0)) num = 0 - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = origin.x + uv.x @@ -136,7 +150,7 @@ def _apply(obj, origin, factor): elif origin == 'CENTER_BOTTOM': origin = Vector((0.0, 100000.0)) num = 0 - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = origin.x + uv.x @@ -145,7 +159,7 @@ def _apply(obj, origin, factor): origin.x = origin.x / num elif origin == 'RIGHT_TOP': origin = Vector((-100000.0, -100000.0)) - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = max(origin.x, uv.x) @@ -153,7 +167,7 @@ def _apply(obj, origin, factor): elif origin == 'RIGHT_CENTER': origin = Vector((-100000.0, 0.0)) num = 0 - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = max(origin.x, uv.x) @@ -162,21 +176,19 @@ def _apply(obj, origin, factor): origin.y = origin.y / num elif origin == 'RIGHT_BOTTOM': origin = Vector((-100000.0, 100000.0)) - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv origin.x = max(origin.x, uv.x) origin.y = min(origin.y, uv.y) # update UV coordinate - for f in sel_faces: + for f in faces: for l in f.loops: uv = l[uv_layer].uv diff = uv - origin l[uv_layer].uv = origin + diff * factor - bmesh.update_edit_mesh(obj.data) - def _get_target_textures(_, __): images = common.find_images(bpy.context.active_object) @@ -207,7 +219,8 @@ class _Properties: ) scene.muv_world_scale_uv_src_uv_area = FloatProperty( name="UV Area", - description="Source UV Area", + description="Source UV Area (Average if calculation method is UV " + "Island or Face)", default=0.0, min=0.0 ) @@ -277,6 +290,26 @@ class _Properties: description="Texture to be applied", items=_get_target_textures ) + scene.muv_world_scale_uv_tgt_area_calc_method = EnumProperty( + name="Area Calculation Method", + description="How to calculate target area", + items=[ + ('MESH', "Mesh", "Calculate area by whole faces in mesh"), + ('UV ISLAND', "UV Island", "Calculate area each UV islands"), + ('FACE', "Face", "Calculate area each face") + ], + default='MESH' + ) + scene.muv_world_scale_uv_measure_only_selected = BoolProperty( + name="Only Selected", + description="Measure with only selected faces", + default=True, + ) + scene.muv_world_scale_uv_apply_only_selected = BoolProperty( + name="Only Selected", + description="Apply to only selected faces", + default=True, + ) @classmethod def del_props(cls, scene): @@ -290,6 +323,9 @@ class _Properties: del scene.muv_world_scale_uv_origin del scene.muv_world_scale_uv_measure_tgt_texture del scene.muv_world_scale_uv_apply_tgt_texture + del scene.muv_world_scale_uv_tgt_area_calc_method + del scene.muv_world_scale_uv_measure_only_selected + del scene.muv_world_scale_uv_apply_only_selected @BlClassRegistry() @@ -304,10 +340,15 @@ class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator): bl_description = "Measure face size for scale calculation" bl_options = {'REGISTER', 'UNDO'} - tgt_texture = StringProperty( + tgt_texture = EnumProperty( name="Texture", - description="Texture to be measured", - default="[Average]" + description="Texture to be applied", + items=_get_target_textures + ) + only_selected = BoolProperty( + name="Only Selected", + description="Measure with only selected faces", + default=True, ) @classmethod @@ -317,32 +358,44 @@ class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator): return True return _is_valid_context(context) + @staticmethod + def setup_argument(ops, scene): + ops.tgt_texture = scene.muv_world_scale_uv_measure_tgt_texture + ops.only_selected = scene.muv_world_scale_uv_measure_only_selected + def execute(self, context): sc = context.scene obj = context.active_object if self.tgt_texture == "[Average]": - uv_area, mesh_area, density = _measure_wsuv_info(obj, 'AVERAGE') + uv_areas, mesh_areas, densities = _measure_wsuv_info( + obj, calc_method='MESH', tex_selection_method='AVERAGE', + only_selected=self.only_selected) elif self.tgt_texture == "[Max]": - uv_area, mesh_area, density = _measure_wsuv_info(obj, 'MAX') + uv_areas, mesh_areas, densities = _measure_wsuv_info( + obj, calc_method='MESH', tex_selection_method='MAX', + only_selected=self.only_selected) elif self.tgt_texture == "[Min]": - uv_area, mesh_area, density = _measure_wsuv_info(obj, 'MIN') + uv_areas, mesh_areas, densities = _measure_wsuv_info( + obj, calc_method='MESH', tex_selection_method='MIN', + only_selected=self.only_selected) else: texture = bpy.data.images[self.tgt_texture] - uv_area, mesh_area, density = _measure_wsuv_info( - obj, 'USER_SPECIFIED', texture.size) - if not uv_area: + uv_areas, mesh_areas, densities = _measure_wsuv_info( + obj, calc_method='MESH', tex_selection_method='USER_SPECIFIED', + only_selected=self.only_selected, tex_size=texture.size) + if not uv_areas: self.report({'WARNING'}, "Object must have more than one UV map and texture") return {'CANCELLED'} - sc.muv_world_scale_uv_src_uv_area = uv_area - sc.muv_world_scale_uv_src_mesh_area = mesh_area - sc.muv_world_scale_uv_src_density = density + sc.muv_world_scale_uv_src_uv_area = uv_areas[0] + sc.muv_world_scale_uv_src_mesh_area = mesh_areas[0] + sc.muv_world_scale_uv_src_density = densities[0] self.report({'INFO'}, "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}" - .format(uv_area, mesh_area, density)) + .format(uv_areas[0], mesh_areas[0], densities[0])) return {'FINISHED'} @@ -395,6 +448,21 @@ class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): default=True, options={'HIDDEN', 'SKIP_SAVE'} ) + tgt_area_calc_method = EnumProperty( + name="Area Calculation Method", + description="How to calculate target area", + items=[ + ('MESH', "Mesh", "Calculate area by whole faces in mesh"), + ('UV ISLAND', "UV Island", "Calculate area each UV islands"), + ('FACE', "Face", "Calculate area each face") + ], + default='MESH' + ) + only_selected = BoolProperty( + name="Only Selected", + description="Apply to only selected faces", + default=True, + ) @classmethod def poll(cls, context): @@ -403,6 +471,16 @@ class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): return True return _is_valid_context(context) + @staticmethod + def setup_argument(ops, scene): + ops.tgt_density = scene.muv_world_scale_uv_tgt_density + ops.tgt_texture_size = scene.muv_world_scale_uv_tgt_texture_size + ops.origin = scene.muv_world_scale_uv_origin + ops.show_dialog = False + ops.tgt_area_calc_method = \ + scene.muv_world_scale_uv_tgt_area_calc_method + ops.only_selected = scene.muv_world_scale_uv_apply_only_selected + def __apply_manual(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) @@ -411,27 +489,47 @@ class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() - tex_size = self.tgt_texture_size - uv_area, _, density = _measure_wsuv_info(obj, 'USER_SPECIFIED', - tex_size) - if not uv_area: + if not bm.loops.layers.uv: self.report({'WARNING'}, "Object must have more than one UV map") return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + tex_layer = common.find_texture_layer(bm) + faces_list = common.get_faces_list( + bm, self.tgt_area_calc_method, self.only_selected) + + tex_size = self.tgt_texture_size + + factors = [] + for faces in faces_list: + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='USER_SPECIFIED', tex_size=tex_size) - tgt_density = self.tgt_density - factor = tgt_density / density + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map") + return {'CANCELLED'} - _apply(context.active_object, self.origin, factor) - self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + tgt_density = self.tgt_density + factor = tgt_density / density + + _apply(faces, uv_layer, self.origin, factor) + factors.append(factor) + + bmesh.update_edit_mesh(obj.data) + self.report({'INFO'}, "Scaling factor: {0}".format(factors)) return {'FINISHED'} def draw(self, _): layout = self.layout - layout.prop(self, "tgt_density") + layout.label(text="Target:") + layout.prop(self, "only_selected") layout.prop(self, "tgt_texture_size") + layout.prop(self, "tgt_density") layout.prop(self, "origin") + layout.prop(self, "tgt_area_calc_method") layout.separator() @@ -500,10 +598,25 @@ class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): default=True, options={'HIDDEN', 'SKIP_SAVE'} ) - tgt_texture = StringProperty( + tgt_texture = EnumProperty( name="Texture", description="Texture to be applied", - default="[Average]" + items=_get_target_textures + ) + tgt_area_calc_method = EnumProperty( + name="Area Calculation Method", + description="How to calculate target area", + items=[ + ('MESH', "Mesh", "Calculate area by whole faces in mesh"), + ('UV ISLAND', "UV Island", "Calculate area each UV islands"), + ('FACE', "Face", "Calculate area each face") + ], + default='MESH' + ) + only_selected = BoolProperty( + name="Only Selected", + description="Apply to only selected faces", + default=True, ) @classmethod @@ -513,6 +626,19 @@ class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): return True return _is_valid_context(context) + @staticmethod + def setup_argument(ops, scene): + ops.tgt_scaling_factor = \ + scene.muv_world_scale_uv_tgt_scaling_factor + ops.origin = scene.muv_world_scale_uv_origin + ops.src_density = scene.muv_world_scale_uv_src_density + ops.same_density = False + ops.show_dialog = False + ops.tgt_texture = scene.muv_world_scale_uv_apply_tgt_texture + ops.tgt_area_calc_method = \ + scene.muv_world_scale_uv_tgt_area_calc_method + ops.only_selected = scene.muv_world_scale_uv_apply_only_selected + def __apply_scaling_density(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) @@ -521,26 +647,49 @@ class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() - if self.tgt_texture == "[Average]": - uv_area, _, density = _measure_wsuv_info(obj, 'AVERAGE') - elif self.tgt_texture == "[Max]": - uv_area, _, density = _measure_wsuv_info(obj, 'MAX') - elif self.tgt_texture == "[Min]": - uv_area, _, density = _measure_wsuv_info(obj, 'MIN') - else: - tgt_texture = bpy.data.images[self.tgt_texture] - uv_area, _, density = _measure_wsuv_info(obj, 'USER_SPECIFIED', - tgt_texture.size) - if not uv_area: - self.report({'WARNING'}, - "Object must have more than one UV map and texture") + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + tex_layer = common.find_texture_layer(bm) + faces_list = common.get_faces_list( + bm, self.tgt_area_calc_method, self.only_selected) + + factors = [] + for faces in faces_list: + if self.tgt_texture == "[Average]": + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='AVERAGE') + elif self.tgt_texture == "[Max]": + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MAX') + elif self.tgt_texture == "[Min]": + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MIN') + else: + tgt_texture = bpy.data.images[self.tgt_texture] + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='USER_SPECIFIED', + tex_size=tgt_texture.size) + + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map and " + "texture") + return {'CANCELLED'} - tgt_density = self.src_density * self.tgt_scaling_factor - factor = tgt_density / density + tgt_density = self.src_density * self.tgt_scaling_factor + factor = tgt_density / density - _apply(context.active_object, self.origin, factor) - self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + _apply(faces, uv_layer, self.origin, factor) + factors.append(factor) + + bmesh.update_edit_mesh(obj.data) + self.report({'INFO'}, "Scaling factor: {0}".format(factors)) return {'FINISHED'} @@ -554,9 +703,13 @@ class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): layout.separator() + layout.label(text="Target:") if not self.same_density: layout.prop(self, "tgt_scaling_factor") + layout.prop(self, "only_selected") + layout.prop(self, "tgt_texture") layout.prop(self, "origin") + layout.prop(self, "tgt_area_calc_method") layout.separator() @@ -640,10 +793,25 @@ class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): default=True, options={'HIDDEN', 'SKIP_SAVE'} ) - tgt_texture = StringProperty( + tgt_texture = EnumProperty( name="Texture", description="Texture to be applied", - default="[Average]" + items=_get_target_textures + ) + tgt_area_calc_method = EnumProperty( + name="Area Calculation Method", + description="How to calculate target area", + items=[ + ('MESH', "Mesh", "Calculate area by whole faces in mesh"), + ('UV ISLAND', "UV Island", "Calculate area each UV islands"), + ('FACE', "Face", "Calculate area each face") + ], + default='MESH' + ) + only_selected = BoolProperty( + name="Only Selected", + description="Apply to only selected faces", + default=True, ) @classmethod @@ -653,6 +821,18 @@ class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): return True return _is_valid_context(context) + @staticmethod + def setup_argument(ops, scene): + ops.origin = scene.muv_world_scale_uv_origin + ops.src_density = scene.muv_world_scale_uv_src_density + ops.src_uv_area = scene.muv_world_scale_uv_src_uv_area + ops.src_mesh_area = scene.muv_world_scale_uv_src_mesh_area + ops.show_dialog = False + ops.tgt_texture = scene.muv_world_scale_uv_apply_tgt_texture + ops.tgt_area_calc_method = \ + scene.muv_world_scale_uv_tgt_area_calc_method + ops.only_selected = scene.muv_world_scale_uv_apply_only_selected + def __apply_proportional_to_mesh(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) @@ -661,28 +841,49 @@ class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() - if self.tgt_texture == "[Average]": - uv_area, mesh_area, density = _measure_wsuv_info(obj, 'AVERAGE') - elif self.tgt_texture == "[Max]": - uv_area, mesh_area, density = _measure_wsuv_info(obj, 'MAX') - elif self.tgt_texture == "[Min]": - uv_area, mesh_area, density = _measure_wsuv_info(obj, 'MIN') - else: - tgt_texture = bpy.data.images[self.tgt_texture] - uv_area, mesh_area, density = _measure_wsuv_info( - obj, 'USER_SPECIFIED', tgt_texture.size) - if not uv_area: - self.report({'WARNING'}, - "Object must have more than one UV map and texture") + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") return {'CANCELLED'} - - tgt_density = self.src_density * sqrt(mesh_area) / sqrt( - self.src_mesh_area) - - factor = tgt_density / density - - _apply(context.active_object, self.origin, factor) - self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + uv_layer = bm.loops.layers.uv.verify() + tex_layer = common.find_texture_layer(bm) + faces_list = common.get_faces_list( + bm, self.tgt_area_calc_method, self.only_selected) + + factors = [] + for faces in faces_list: + if self.tgt_texture == "[Average]": + uv_area, mesh_area, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='AVERAGE') + elif self.tgt_texture == "[Max]": + uv_area, mesh_area, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MAX') + elif self.tgt_texture == "[Min]": + uv_area, mesh_area, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MIN') + else: + tgt_texture = bpy.data.images[self.tgt_texture] + uv_area, mesh_area, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='USER_SPECIFIED', + tex_size=tgt_texture.size) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map and " + "texture") + return {'CANCELLED'} + + tgt_density = self.src_density * sqrt(mesh_area) / sqrt( + self.src_mesh_area) + factor = tgt_density / density + + _apply(faces, uv_layer, self.origin, factor) + factors.append(factor) + + bmesh.update_edit_mesh(obj.data) + self.report({'INFO'}, "Scaling factor: {0}".format(factors)) return {'FINISHED'} @@ -697,7 +898,12 @@ class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): col.enabled = False layout.separator() + + layout.label(text="Target:") + layout.prop(self, "only_selected") layout.prop(self, "origin") + layout.prop(self, "tgt_area_calc_method") + layout.prop(self, "tgt_texture") layout.separator() diff --git a/magic_uv/preferences.py b/magic_uv/preferences.py index 6d66b308..926ec728 100644 --- a/magic_uv/preferences.py +++ b/magic_uv/preferences.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy from bpy.props import ( @@ -33,6 +33,7 @@ from bpy.props import ( from bpy.types import AddonPreferences from . import common +from .op.clip_uv import MUV_OT_ClipUV from .op.flip_rotate_uv import MUV_OT_FlipRotateUV from .op.mirror_uv import MUV_OT_MirrorUV from .op.move_uv import MUV_OT_MoveUV @@ -122,13 +123,17 @@ def image_uvs_menu_fn(self, context): sc = context.scene layout.separator() - # Copy/Paste UV (on UV/Image Editor) layout.label(text="Copy/Paste UV", icon=compat.icon('IMAGE')) + # Copy/Paste UV (on UV/Image Editor) layout.menu(MUV_MT_CopyPasteUV_UVEdit.bl_idname, text="Copy/Paste UV") layout.separator() - # Pack UV layout.label(text="UV Manipulation", icon=compat.icon('IMAGE')) + # Clip UV + ops = layout.operator(MUV_OT_ClipUV.bl_idname, text="Clip UV") + ops.clip_uv_range_max = sc.muv_clip_uv_range_max + ops.clip_uv_range_min = sc.muv_clip_uv_range_min + # Pack UV ops = layout.operator(MUV_OT_PackUV.bl_idname, text="Pack UV") ops.allowable_center_deviation = sc.muv_pack_uv_allowable_center_deviation ops.allowable_size_deviation = sc.muv_pack_uv_allowable_size_deviation @@ -143,8 +148,8 @@ def image_uvs_menu_fn(self, context): layout.menu(MUV_MT_AlignUV.bl_idname, text="Align UV") layout.separator() - # Align UV Cursor layout.label(text="Editor Enhancement", icon=compat.icon('IMAGE')) + # Align UV Cursor layout.menu(MUV_MT_AlignUVCursor.bl_idname, text="Align UV Cursor") # UV Bounding Box layout.prop(sc, "muv_uv_bounding_box_show", text="UV Bounding Box") diff --git a/magic_uv/properites.py b/magic_uv/properites.py index e553816b..b269cbed 100644 --- a/magic_uv/properites.py +++ b/magic_uv/properites.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from .utils.property_class_registry import PropertyClassRegistry diff --git a/magic_uv/ui/IMAGE_MT_uvs.py b/magic_uv/ui/IMAGE_MT_uvs.py index 74e796cc..00d95d9e 100644 --- a/magic_uv/ui/IMAGE_MT_uvs.py +++ b/magic_uv/ui/IMAGE_MT_uvs.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy @@ -39,7 +39,10 @@ from ..op.select_uv import ( MUV_OT_SelectUV_SelectOverlapped, MUV_OT_SelectUV_SelectFlipped, ) -from ..op.uv_inspection import MUV_OT_UVInspection_Update +from ..op.uv_inspection import ( + MUV_OT_UVInspection_Update, + MUV_OT_UVInspection_PaintUVIsland, +) from ..utils.bl_class_registry import BlClassRegistry @@ -184,5 +187,8 @@ class MUV_MT_UVInspection(bpy.types.Menu): layout = self.layout sc = context.scene - layout.prop(sc, "muv_uv_inspection_show", text="UV Inspection") + layout.prop(sc, "muv_uv_inspection_show", + text="Show Overlapped/Flipped") layout.operator(MUV_OT_UVInspection_Update.bl_idname, text="Update") + layout.separator() + layout.operator(MUV_OT_UVInspection_PaintUVIsland.bl_idname) diff --git a/magic_uv/ui/VIEW3D_MT_object.py b/magic_uv/ui/VIEW3D_MT_object.py index b4fca522..f34c74f9 100644 --- a/magic_uv/ui/VIEW3D_MT_object.py +++ b/magic_uv/ui/VIEW3D_MT_object.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy diff --git a/magic_uv/ui/VIEW3D_MT_uv_map.py b/magic_uv/ui/VIEW3D_MT_uv_map.py index 853d1855..7ab50ace 100644 --- a/magic_uv/ui/VIEW3D_MT_uv_map.py +++ b/magic_uv/ui/VIEW3D_MT_uv_map.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy.utils @@ -147,24 +147,25 @@ class MUV_MT_WorldScaleUV(bpy.types.Menu): layout = self.layout sc = context.scene - layout.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, - text="Measure") + layout.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, text="Measure") - layout.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname, - text="Apply (Manual)") + ops = layout.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname, + text="Apply (Manual)") + ops.show_dialog = True ops = layout.operator( MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, text="Apply (Same Desity)") ops.src_density = sc.muv_world_scale_uv_src_density ops.same_density = True + ops.show_dialog = True ops = layout.operator( MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, text="Apply (Scaling Desity)") ops.src_density = sc.muv_world_scale_uv_src_density ops.same_density = False - ops.tgt_scaling_factor = sc.muv_world_scale_uv_tgt_scaling_factor + ops.show_dialog = True ops = layout.operator( MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname, @@ -172,7 +173,7 @@ class MUV_MT_WorldScaleUV(bpy.types.Menu): ops.src_density = sc.muv_world_scale_uv_src_density ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area - ops.origin = sc.muv_world_scale_uv_origin + ops.show_dialog = True @BlClassRegistry() diff --git a/magic_uv/ui/__init__.py b/magic_uv/ui/__init__.py index 50049251..bb16a847 100644 --- a/magic_uv/ui/__init__.py +++ b/magic_uv/ui/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" if "bpy" in locals(): import importlib diff --git a/magic_uv/ui/uvedit_copy_paste_uv.py b/magic_uv/ui/uvedit_copy_paste_uv.py index 987a24a0..211737c8 100644 --- a/magic_uv/ui/uvedit_copy_paste_uv.py +++ b/magic_uv/ui/uvedit_copy_paste_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy diff --git a/magic_uv/ui/uvedit_editor_enhancement.py b/magic_uv/ui/uvedit_editor_enhancement.py index 6639650c..f98e5193 100644 --- a/magic_uv/ui/uvedit_editor_enhancement.py +++ b/magic_uv/ui/uvedit_editor_enhancement.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy @@ -32,6 +32,7 @@ from ..op.uv_bounding_box import ( from ..op.uv_inspection import ( MUV_OT_UVInspection_Render, MUV_OT_UVInspection_Update, + MUV_OT_UVInspection_PaintUVIsland, ) from ..utils.bl_class_registry import BlClassRegistry from ..utils import compatibility as compat @@ -143,3 +144,5 @@ class MUV_PT_UVEdit_EditorEnhancement(bpy.types.Panel): row.prop(sc, "muv_uv_inspection_show_flipped") row = box.row() row.prop(sc, "muv_uv_inspection_show_mode") + box.separator() + box.operator(MUV_OT_UVInspection_PaintUVIsland.bl_idname) diff --git a/magic_uv/ui/uvedit_uv_manipulation.py b/magic_uv/ui/uvedit_uv_manipulation.py index 5589b73e..79a1731a 100644 --- a/magic_uv/ui/uvedit_uv_manipulation.py +++ b/magic_uv/ui/uvedit_uv_manipulation.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy @@ -38,6 +38,7 @@ from ..op.select_uv import ( MUV_OT_SelectUV_SelectFlipped, ) from ..op.pack_uv import MUV_OT_PackUV +from ..op.clip_uv import MUV_OT_ClipUV from ..utils.bl_class_registry import BlClassRegistry from ..utils import compatibility as compat @@ -129,3 +130,16 @@ class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel): box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="") box.label(text="Allowable Size Deviation:") box.prop(sc, "muv_pack_uv_allowable_size_deviation", text="") + + box = layout.box() + box.prop(sc, "muv_clip_uv_enabled", text="Clip UV") + if sc.muv_clip_uv_enabled: + ops = box.operator(MUV_OT_ClipUV.bl_idname, text="Clip UV") + ops.clip_uv_range_max = sc.muv_clip_uv_range_max + ops.clip_uv_range_min = sc.muv_clip_uv_range_min + box.label(text="Range:") + row = box.row() + col = row.column() + col.prop(sc, "muv_clip_uv_range_max", text="Max") + col = row.column() + col.prop(sc, "muv_clip_uv_range_min", text="Min") diff --git a/magic_uv/ui/view3d_copy_paste_uv_editmode.py b/magic_uv/ui/view3d_copy_paste_uv_editmode.py index 041f279d..0c7273a3 100644 --- a/magic_uv/ui/view3d_copy_paste_uv_editmode.py +++ b/magic_uv/ui/view3d_copy_paste_uv_editmode.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy diff --git a/magic_uv/ui/view3d_copy_paste_uv_objectmode.py b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py index 21d2bc4c..b2a33e9a 100644 --- a/magic_uv/ui/view3d_copy_paste_uv_objectmode.py +++ b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy diff --git a/magic_uv/ui/view3d_uv_manipulation.py b/magic_uv/ui/view3d_uv_manipulation.py index 3a694008..1d10eb65 100644 --- a/magic_uv/ui/view3d_uv_manipulation.py +++ b/magic_uv/ui/view3d_uv_manipulation.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy @@ -113,17 +113,13 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel): box.prop(sc, "muv_world_scale_uv_mode", text="") if sc.muv_world_scale_uv_mode == 'MANUAL': - sp = compat.layout_split(box, 0.4) - col = sp.column(align=True) - col.label(text="Target:") - sp = compat.layout_split(sp, 1.0) - col = sp.column(align=True) - ops = col.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname, + box.label(text="Target:") + row = box.row(align=True) + ops = row.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname, text="Apply") - ops.tgt_density = sc.muv_world_scale_uv_tgt_density - ops.tgt_texture_size = sc.muv_world_scale_uv_tgt_texture_size - ops.origin = sc.muv_world_scale_uv_origin + MUV_OT_WorldScaleUV_ApplyManual.setup_argument(ops, sc) ops.show_dialog = False + row.prop(sc, "muv_world_scale_uv_apply_only_selected") sp = compat.layout_split(box, 0.5) col = sp.column() col.prop(sc, "muv_world_scale_uv_tgt_texture_size", @@ -133,16 +129,15 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel): col.label(text="Density:") col.prop(sc, "muv_world_scale_uv_tgt_density") box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + box.prop(sc, "muv_world_scale_uv_tgt_area_calc_method") elif sc.muv_world_scale_uv_mode == 'SAME_DENSITY': - sp = compat.layout_split(box, 0.4) - col = sp.column(align=True) - col.label(text="Source:") - sp = compat.layout_split(sp, 1.0) - col = sp.column(align=True) - ops = col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, + box.label(text="Source:") + row = box.row(align=True) + ops = row.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, text="Measure") - ops.tgt_texture = sc.muv_world_scale_uv_measure_tgt_texture + MUV_OT_WorldScaleUV_Measure.setup_argument(ops, sc) + row.prop(sc, "muv_world_scale_uv_measure_only_selected") col = box.column(align=True) col.prop(sc, "muv_world_scale_uv_measure_tgt_texture") sp = compat.layout_split(box, 0.7) @@ -154,30 +149,27 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel): col.label(text="px2/cm2") box.separator() - sp = compat.layout_split(box, 0.4) - col = sp.column(align=True) - col.label(text="Target:") - sp = compat.layout_split(sp, 1.0) - col = sp.column(align=True) - ops = col.operator( + + box.label(text="Target:") + row = box.row(align=True) + ops = row.operator( MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, text="Apply") - ops.src_density = sc.muv_world_scale_uv_src_density - ops.origin = sc.muv_world_scale_uv_origin + MUV_OT_WorldScaleUV_ApplyScalingDensity.setup_argument(ops, sc) ops.same_density = True ops.show_dialog = False - ops.tgt_texture = sc.muv_world_scale_uv_apply_tgt_texture + row.prop(sc, "muv_world_scale_uv_apply_only_selected") + box.prop(sc, "muv_world_scale_uv_apply_tgt_texture") box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + box.prop(sc, "muv_world_scale_uv_tgt_area_calc_method") elif sc.muv_world_scale_uv_mode == 'SCALING_DENSITY': - sp = compat.layout_split(box, 0.4) - col = sp.column(align=True) - col.label(text="Source:") - sp = compat.layout_split(sp, 1.0) - col = sp.column(align=True) - ops = col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, + box.label(text="Source:") + row = box.row(align=True) + ops = row.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, text="Measure") - ops.tgt_texture = sc.muv_world_scale_uv_measure_tgt_texture + MUV_OT_WorldScaleUV_Measure.setup_argument(ops, sc) + row.prop(sc, "muv_world_scale_uv_measure_only_selected") col = box.column(align=True) col.prop(sc, "muv_world_scale_uv_measure_tgt_texture") sp = compat.layout_split(box, 0.7) @@ -189,34 +181,29 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel): col.label(text="px2/cm2") box.separator() - sp = compat.layout_split(box, 0.4) - col = sp.column(align=True) - col.label(text="Target:") - sp = compat.layout_split(sp, 1.0) - col = sp.column(align=True) - ops = col.operator( + + box.label(text="Target:") + row = box.row(align=True) + ops = row.operator( MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, text="Apply") - ops.src_density = sc.muv_world_scale_uv_src_density - ops.origin = sc.muv_world_scale_uv_origin + MUV_OT_WorldScaleUV_ApplyScalingDensity.setup_argument(ops, sc) ops.same_density = False ops.show_dialog = False - ops.tgt_scaling_factor = \ - sc.muv_world_scale_uv_tgt_scaling_factor - ops.tgt_texture = sc.muv_world_scale_uv_apply_tgt_texture + row.prop(sc, "muv_world_scale_uv_apply_only_selected") + box.prop(sc, "muv_world_scale_uv_apply_tgt_texture") box.prop(sc, "muv_world_scale_uv_tgt_scaling_factor", text="Scaling Factor") box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + box.prop(sc, "muv_world_scale_uv_tgt_area_calc_method") elif sc.muv_world_scale_uv_mode == 'PROPORTIONAL_TO_MESH': - sp = compat.layout_split(box, 0.4) - col = sp.column(align=True) - col.label(text="Source:") - sp = compat.layout_split(sp, 1.0) - col = sp.column(align=True) - ops = col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, + box.label(text="Source:") + row = box.row(align=True) + ops = row.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, text="Measure") - ops.tgt_texture = sc.muv_world_scale_uv_measure_tgt_texture + MUV_OT_WorldScaleUV_Measure.setup_argument(ops, sc) + row.prop(sc, "muv_world_scale_uv_measure_only_selected") col = box.column(align=True) col.prop(sc, "muv_world_scale_uv_measure_tgt_texture") sp = compat.layout_split(box, 0.7) @@ -234,24 +221,19 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel): col.enabled = False box.separator() - sp = compat.layout_split(box, 0.4) - col = sp.column(align=True) - col.label(text="Target:") - sp = compat.layout_split(sp, 1.0) - col = sp.column(align=True) - ops = col.operator( + + box.label(text="Target:") + row = box.row(align=True) + ops = row.operator( MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname, text="Apply") - ops.src_density = sc.muv_world_scale_uv_src_density - ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area - ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area - ops.origin = sc.muv_world_scale_uv_origin + MUV_OT_WorldScaleUV_ApplyProportionalToMesh.setup_argument( + ops, sc) ops.show_dialog = False - ops.tgt_texture = sc.muv_world_scale_uv_apply_tgt_texture + row.prop(sc, "muv_world_scale_uv_apply_only_selected") + box.prop(sc, "muv_world_scale_uv_apply_tgt_texture") box.prop(sc, "muv_world_scale_uv_origin", text="Origin") - - col = box.column(align=True) - col.prop(sc, "muv_world_scale_uv_apply_tgt_texture") + box.prop(sc, "muv_world_scale_uv_tgt_area_calc_method") box = layout.box() box.prop(sc, "muv_preserve_uv_aspect_enabled", diff --git a/magic_uv/ui/view3d_uv_mapping.py b/magic_uv/ui/view3d_uv_mapping.py index 0e31620b..4344adb7 100644 --- a/magic_uv/ui/view3d_uv_mapping.py +++ b/magic_uv/ui/view3d_uv_mapping.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy diff --git a/magic_uv/updater.py b/magic_uv/updater.py index d522c009..8d610b16 100644 --- a/magic_uv/updater.py +++ b/magic_uv/updater.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import os diff --git a/magic_uv/utils/__init__.py b/magic_uv/utils/__init__.py index 0e6ef744..c96b9225 100644 --- a/magic_uv/utils/__init__.py +++ b/magic_uv/utils/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" if "bpy" in locals(): import importlib diff --git a/magic_uv/utils/addon_updater.py b/magic_uv/utils/addon_updater.py index 2f3d0c0f..5df59fd4 100644 --- a/magic_uv/utils/addon_updater.py +++ b/magic_uv/utils/addon_updater.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from threading import Lock import urllib @@ -60,7 +60,7 @@ def _request(url, json_decode=True): return json.JSONDecoder().decode(data.decode()) except Exception as e: raise RuntimeError("API response has invalid JSON format ({})" - .format(str(e.reason))) + .format(str(e))) return data.decode() @@ -153,7 +153,7 @@ def _compare_version(ver1, ver2): if v1[idx] > v2[idx]: return 1 # v1 > v2 - elif v1[idx] < v2[idx]: + if v1[idx] < v2[idx]: return -1 # v1 < v2 return comp(v1, v2, idx + 1) diff --git a/magic_uv/utils/bl_class_registry.py b/magic_uv/utils/bl_class_registry.py index 826f1483..f9f05faf 100644 --- a/magic_uv/utils/bl_class_registry.py +++ b/magic_uv/utils/bl_class_registry.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy diff --git a/magic_uv/utils/compatibility.py b/magic_uv/utils/compatibility.py index 6b7da000..b4c7c4ea 100644 --- a/magic_uv/utils/compatibility.py +++ b/magic_uv/utils/compatibility.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" import bpy import bgl diff --git a/magic_uv/utils/property_class_registry.py b/magic_uv/utils/property_class_registry.py index dff4712f..9caa735c 100644 --- a/magic_uv/utils/property_class_registry.py +++ b/magic_uv/utils/property_class_registry.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "6.2" -__date__ = "31 Jul 2019" +__version__ = "6.3" +__date__ = "10 Aug 2020" from .. import common -- cgit v1.2.3 From 1d1bb1a707554ce51696d85d2feb9718e34a0220 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 11 Aug 2020 00:02:52 -0400 Subject: Collection Manager: Object selection. Task: T69577 Adds the ability to select all object in a QCD slot. Alt+LMB deselects everything and selects all objects in the QCD slot. Alt+Shift+LMB adds/removes objects in the QCD slot to/from the selection. Added a selection operator for use in the Collection Manager popup. (currently unused) --- object_collection_manager/__init__.py | 3 +- object_collection_manager/operator_utils.py | 48 ++++++++++++++++++++------ object_collection_manager/operators.py | 53 +++++++++++++++++++++++++++++ object_collection_manager/qcd_operators.py | 24 ++++++++++--- 4 files changed, 112 insertions(+), 16 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 09cb1c2c..49bd0de9 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 12, 5), + "version": (2, 13, 0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -111,6 +111,7 @@ classes = ( operators.CMNewCollectionOperator, operators.CMRemoveCollectionOperator, operators.CMRemoveEmptyCollectionsOperator, + operators.CMSelectCollectionObjectsOperator, operators.CMSetCollectionOperator, operators.CMPhantomModeOperator, operators.CMApplyPhantomModeOperator, diff --git a/object_collection_manager/operator_utils.py b/object_collection_manager/operator_utils.py index d86a534f..ed7fce09 100644 --- a/object_collection_manager/operator_utils.py +++ b/object_collection_manager/operator_utils.py @@ -28,6 +28,7 @@ from .internals import ( copy_buffer, swap_buffer, update_property_group, + get_move_selection, ) rto_path = { @@ -57,20 +58,21 @@ def set_rto(layer_collection, rto, value): setattr(collection, rto_path[rto].split(".")[1], value) -def apply_to_children(laycol, apply_function): - laycol_iter_list = [laycol.children] +def apply_to_children(parent, apply_function): + # works for both Collections & LayerCollections + child_lists = [parent.children] - while len(laycol_iter_list) > 0: - new_laycol_iter_list = [] + while child_lists: + new_child_lists = [] - for laycol_iter in laycol_iter_list: - for layer_collection in laycol_iter: - apply_function(layer_collection) + for child_list in child_lists: + for child in child_list: + apply_function(child) - if len(layer_collection.children) > 0: - new_laycol_iter_list.append(layer_collection.children) + if child.children: + new_child_lists.append(child.children) - laycol_iter_list = new_laycol_iter_list + child_lists = new_child_lists def isolate_rto(cls, self, view_layer, rto, *, children=False): @@ -371,3 +373,29 @@ def remove_collection(laycol, collection, context): laycol = laycol["parent"] cm.cm_list_index = laycol["row_index"] + + +def select_collection_objects(collection_index, collection_name, replace, nested): + if collection_index == 0: + target_collection = bpy.context.view_layer.layer_collection.collection + + else: + laycol = layer_collections[collection_name] + target_collection = laycol["ptr"].collection + + if replace: + bpy.ops.object.select_all(action='DESELECT') + + selection_state = get_move_selection().isdisjoint(target_collection.objects) + + def select_objects(collection): + for obj in collection.objects: + try: + obj.select_set(selection_state) + except RuntimeError: + pass + + select_objects(target_collection) + + if nested: + apply_to_children(target_collection, select_objects) diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 46e83023..7509d3f2 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -65,6 +65,7 @@ from .operator_utils import ( clear_swap, link_child_collections_to_parent, remove_collection, + select_collection_objects, ) class SetActiveCollection(Operator): @@ -215,6 +216,58 @@ class ExpandSublevelOperator(Operator): return {'FINISHED'} +class CMSelectCollectionObjectsOperator(Operator): + bl_label = "Select All Objects in the Collection" + bl_description = ( + " * LMB - Select all objects in collection.\n" + " * Shift+LMB - Add/Remove collection objects from selection.\n" + " * Ctrl+LMB - Isolate nested selection.\n" + " * Ctrl+Shift+LMB - Add/Remove nested from selection" + ) + bl_idname = "view3d.select_collection_objects" + bl_options = {'REGISTER', 'UNDO'} + + collection_index: IntProperty() + collection_name: StringProperty() + + def invoke(self, context, event): + modifiers = get_modifiers(event) + + if modifiers == {"shift"}: + select_collection_objects( + collection_index=self.collection_index, + collection_name=self.collection_name, + replace=False, + nested=False + ) + + elif modifiers == {"ctrl"}: + select_collection_objects( + collection_index=self.collection_index, + collection_name=self.collection_name, + replace=True, + nested=True + ) + + elif modifiers == {"ctrl", "shift"}: + select_collection_objects( + collection_index=self.collection_index, + collection_name=self.collection_name, + replace=False, + nested=True + ) + + else: + select_collection_objects( + collection_index=self.collection_index, + collection_name=self.collection_name, + replace=True, + nested=False + ) + + return {'FINISHED'} + + class CMSetCollectionOperator(Operator): bl_label = "Set Object Collection" bl_description = ( diff --git a/object_collection_manager/qcd_operators.py b/object_collection_manager/qcd_operators.py index 4f5f555a..4ffec562 100644 --- a/object_collection_manager/qcd_operators.py +++ b/object_collection_manager/qcd_operators.py @@ -45,6 +45,7 @@ from .internals import ( from .operator_utils import ( apply_to_children, + select_collection_objects, ) @@ -157,19 +158,32 @@ class ViewMoveQCDSlot(Operator): if modifiers == {"shift"}: bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=True) - return {'FINISHED'} - elif modifiers == {"ctrl"}: bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=False) - return {'FINISHED'} elif modifiers == {"ctrl", "shift"}: bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=True) - return {'FINISHED'} + + elif modifiers == {"alt"}: + select_collection_objects( + collection_index=None, + collection_name=qcd_slots.get_name(self.slot), + replace=True, + nested=False + ) + + elif modifiers == {"alt", "shift"}: + select_collection_objects( + collection_index=None, + collection_name=qcd_slots.get_name(self.slot), + replace=False, + nested=False + ) else: bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=False) - return {'FINISHED'} + + return {'FINISHED'} class ViewQCDSlot(Operator): '''View objects in QCD slot''' -- cgit v1.2.3 From 559fbf908ba8721f4c9e72b6dc7c3bfa0863479f Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 11 Aug 2020 00:19:34 -0400 Subject: Collection Manager: Add Holdout & Indirect Only. Task: T69577 Add support for the Holdout and Indirect Only RTOs. --- object_collection_manager/__init__.py | 8 +- object_collection_manager/internals.py | 22 ++- object_collection_manager/operator_utils.py | 130 ++++++++++++++--- object_collection_manager/operators.py | 209 ++++++++++++++++++++++++++++ object_collection_manager/ui.py | 68 ++++++++- 5 files changed, 413 insertions(+), 24 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 49bd0de9..d7b312a5 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 13, 0), + "version": (2, 14, 0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -77,6 +77,8 @@ class CollectionManagerProperties(PropertyGroup): show_hide_viewport: BoolProperty(default=True, name="[VV] Hide in Viewport") show_disable_viewport: BoolProperty(default=False, name="[DV] Disable in Viewports") show_render: BoolProperty(default=False, name="[RR] Disable in Renders") + show_holdout: BoolProperty(default=False, name="[HH] Holdout") + show_indirect_only: BoolProperty(default=False, name="[IO] Indirect Only") align_local_ops: BoolProperty(default=False, name="Align Local Options", description="Align local options in a column to the right") @@ -108,6 +110,10 @@ classes = ( operators.CMUnDisableViewportAllOperator, operators.CMDisableRenderOperator, operators.CMUnDisableRenderAllOperator, + operators.CMHoldoutOperator, + operators.CMUnHoldoutAllOperator, + operators.CMIndirectOnlyOperator, + operators.CMUnIndirectOnlyAllOperator, operators.CMNewCollectionOperator, operators.CMRemoveCollectionOperator, operators.CMRemoveEmptyCollectionsOperator, diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py index 857e73aa..163e9804 100644 --- a/object_collection_manager/internals.py +++ b/object_collection_manager/internals.py @@ -53,12 +53,16 @@ rto_history = { "disable": {}, "disable_all": {}, "render": {}, - "render_all": {} + "render_all": {}, + "holdout": {}, + "holdout_all": {}, + "indirect": {}, + "indirect_all": {}, } expand_history = { "target": "", - "history": [] + "history": [], } phantom_history = { @@ -70,12 +74,16 @@ phantom_history = { "hide_history": {}, "disable_history": {}, "render_history": {}, + "holdout_history": {}, + "indirect_history": {}, "exclude_all_history": [], "select_all_history": [], "hide_all_history": [], "disable_all_history": [], - "render_all_history": [] + "render_all_history": [], + "holdout_all_history": [], + "indirect_all_history": [], } copy_buffer = { @@ -317,7 +325,9 @@ def update_col_name(self, context): "select", "hide", "disable", - "render" + "render", + "holdout", + "indirect", ] orig_targets = { @@ -584,6 +594,8 @@ def generate_state(): "hide": [], "disable": [], "render": [], + "holdout": [], + "indirect": [], } for name, laycol in layer_collections.items(): @@ -593,6 +605,8 @@ def generate_state(): state["hide"].append(laycol["ptr"].hide_viewport) state["disable"].append(laycol["ptr"].collection.hide_viewport) state["render"].append(laycol["ptr"].collection.hide_render) + state["holdout"].append(laycol["ptr"].holdout) + state["indirect"].append(laycol["ptr"].indirect_only) return state diff --git a/object_collection_manager/operator_utils.py b/object_collection_manager/operator_utils.py index ed7fce09..6f7ee83b 100644 --- a/object_collection_manager/operator_utils.py +++ b/object_collection_manager/operator_utils.py @@ -36,12 +36,67 @@ rto_path = { "select": "collection.hide_select", "hide": "hide_viewport", "disable": "collection.hide_viewport", - "render": "collection.hide_render" + "render": "collection.hide_render", + "holdout": "holdout", + "indirect": "indirect_only", + } + +set_off_on = { + "exclude": { + "off": True, + "on": False + }, + "select": { + "off": True, + "on": False + }, + "hide": { + "off": True, + "on": False + }, + "disable": { + "off": True, + "on": False + }, + "render": { + "off": True, + "on": False + }, + "holdout": { + "off": False, + "on": True + }, + "indirect": { + "off": False, + "on": True + } + } + +get_off_on = { + False: { + "exclude": "on", + "select": "on", + "hide": "on", + "disable": "on", + "render": "on", + "holdout": "off", + "indirect": "off", + }, + + True: { + "exclude": "off", + "select": "off", + "hide": "off", + "disable": "off", + "render": "off", + "holdout": "on", + "indirect": "on", + } } def get_rto(layer_collection, rto): - if rto in ["exclude", "hide"]: + if rto in ["exclude", "hide", "holdout", "indirect"]: return getattr(layer_collection, rto_path[rto]) else: @@ -50,7 +105,7 @@ def get_rto(layer_collection, rto): def set_rto(layer_collection, rto, value): - if rto in ["exclude", "hide"]: + if rto in ["exclude", "hide", "holdout", "indirect"]: setattr(layer_collection, rto_path[rto], value) else: @@ -76,13 +131,16 @@ def apply_to_children(parent, apply_function): def isolate_rto(cls, self, view_layer, rto, *, children=False): + off = set_off_on[rto]["off"] + on = set_off_on[rto]["on"] + laycol_ptr = layer_collections[self.name]["ptr"] target = rto_history[rto][view_layer]["target"] history = rto_history[rto][view_layer]["history"] # get active collections active_layer_collections = [x["ptr"] for x in layer_collections.values() - if not get_rto(x["ptr"], rto)] + if get_rto(x["ptr"], rto) == on] # check if previous state should be restored if cls.isolated and self.name == target: @@ -100,7 +158,7 @@ def isolate_rto(cls, self, view_layer, rto, *, children=False): active_layer_collections[0].name == self.name): # activate all collections for item in layer_collections.values(): - set_rto(item["ptr"], rto, False) + set_rto(item["ptr"], rto, on) # reset target and history del rto_history[rto][view_layer] @@ -130,15 +188,15 @@ def isolate_rto(cls, self, view_layer, rto, *, children=False): # isolate collection for item in layer_collections.values(): if item["name"] != laycol_ptr.name: - set_rto(item["ptr"], rto, True) + set_rto(item["ptr"], rto, off) - set_rto(laycol_ptr, rto, False) + set_rto(laycol_ptr, rto, on) - if rto != "exclude": + if rto not in ["exclude", "holdout", "indirect"]: # activate all parents laycol = layer_collections[self.name] while laycol["id"] != 0: - set_rto(laycol["ptr"], rto, False) + set_rto(laycol["ptr"], rto, on) laycol = laycol["parent"] if children: @@ -156,7 +214,7 @@ def isolate_rto(cls, self, view_layer, rto, *, children=False): apply_to_children(laycol_ptr, restore_child_states) - else: + elif rto == "exclude": # deactivate all children def deactivate_all_children(layer_collection): set_rto(layer_collection, rto, True) @@ -183,6 +241,9 @@ def toggle_children(self, view_layer, rto): def activate_all_rtos(view_layer, rto): + off = set_off_on[rto]["off"] + on = set_off_on[rto]["on"] + history = rto_history[rto+"_all"][view_layer] # if not activated, activate all @@ -190,12 +251,12 @@ def activate_all_rtos(view_layer, rto): keep_history = False for item in reversed(list(layer_collections.values())): - if get_rto(item["ptr"], rto) == True: + if get_rto(item["ptr"], rto) == off: keep_history = True history.append(get_rto(item["ptr"], rto)) - set_rto(item["ptr"], rto, False) + set_rto(item["ptr"], rto, on) if not keep_history: history.clear() @@ -233,12 +294,22 @@ def copy_rtos(view_layer, rto): # copy copy_buffer["RTO"] = rto for laycol in layer_collections.values(): - copy_buffer["values"].append(get_rto(laycol["ptr"], rto)) + copy_buffer["values"].append(get_off_on[ + get_rto(laycol["ptr"], rto) + ][ + rto + ] + ) else: # paste for x, laycol in enumerate(layer_collections.values()): - set_rto(laycol["ptr"], rto, copy_buffer["values"][x]) + set_rto(laycol["ptr"], + rto, + set_off_on[rto][ + copy_buffer["values"][x] + ] + ) # clear rto history rto_history[rto].pop(view_layer, None) @@ -254,18 +325,41 @@ def swap_rtos(view_layer, rto): # get A swap_buffer["A"]["RTO"] = rto for laycol in layer_collections.values(): - swap_buffer["A"]["values"].append(get_rto(laycol["ptr"], rto)) + swap_buffer["A"]["values"].append(get_off_on[ + get_rto(laycol["ptr"], rto) + ][ + rto + ] + ) else: # get B swap_buffer["B"]["RTO"] = rto for laycol in layer_collections.values(): - swap_buffer["B"]["values"].append(get_rto(laycol["ptr"], rto)) + swap_buffer["B"]["values"].append(get_off_on[ + get_rto(laycol["ptr"], rto) + ][ + rto + ] + ) # swap A with B for x, laycol in enumerate(layer_collections.values()): - set_rto(laycol["ptr"], swap_buffer["A"]["RTO"], swap_buffer["B"]["values"][x]) - set_rto(laycol["ptr"], swap_buffer["B"]["RTO"], swap_buffer["A"]["values"][x]) + set_rto(laycol["ptr"], swap_buffer["A"]["RTO"], + set_off_on[ + swap_buffer["A"]["RTO"] + ][ + swap_buffer["B"]["values"][x] + ] + ) + + set_rto(laycol["ptr"], swap_buffer["B"]["RTO"], + set_off_on[ + swap_buffer["B"]["RTO"] + ][ + swap_buffer["A"]["values"][x] + ] + ) # clear rto history diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 7509d3f2..ff1bbf95 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -909,6 +909,211 @@ class CMUnDisableRenderAllOperator(Operator): return {'FINISHED'} +class CMHoldoutOperator(Operator): + bl_label = "[HH] Holdout" + bl_description = ( + " * Shift+LMB - Isolate/Restore.\n" + " * Shift+Ctrl+LMB - Isolate nested/Restore.\n" + " * Ctrl+LMB - Toggle nested.\n" + " * Alt+LMB - Discard history" + ) + bl_idname = "view3d.holdout_collection" + bl_options = {'REGISTER', 'UNDO'} + + name: StringProperty() + + # static class var + isolated = False + + def invoke(self, context, event): + global rto_history + cls = CMHoldoutOperator + + modifiers = get_modifiers(event) + view_layer = context.view_layer.name + laycol_ptr = layer_collections[self.name]["ptr"] + + if not view_layer in rto_history["holdout"]: + rto_history["holdout"][view_layer] = {"target": "", "history": []} + + if modifiers == {"alt"}: + del rto_history["holdout"][view_layer] + cls.isolated = False + + elif modifiers == {"shift"}: + isolate_rto(cls, self, view_layer, "holdout") + + elif modifiers == {"ctrl"}: + toggle_children(self, view_layer, "holdout") + + cls.isolated = False + + elif modifiers == {"ctrl", "shift"}: + isolate_rto(cls, self, view_layer, "holdout", children=True) + + else: + # toggle holdout + + # reset holdout history + del rto_history["holdout"][view_layer] + + # toggle holdout of collection in viewport + laycol_ptr.holdout = not laycol_ptr.holdout + + cls.isolated = False + + # reset holdout all history + if view_layer in rto_history["holdout_all"]: + del rto_history["holdout_all"][view_layer] + + return {'FINISHED'} + + +class CMUnHoldoutAllOperator(Operator): + bl_label = "[HH Global] Holdout" + bl_description = ( + " * LMB - Enable all/Restore.\n" + " * Shift+LMB - Invert.\n" + " * Ctrl+LMB - Copy/Paste RTOs.\n" + " * Ctrl+Alt+LMB - Swap RTOs.\n" + " * Alt+LMB - Discard history" + ) + bl_idname = "view3d.un_holdout_all_collections" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + global rto_history + + view_layer = context.view_layer.name + modifiers = get_modifiers(event) + + if not view_layer in rto_history["holdout_all"]: + rto_history["holdout_all"][view_layer] = [] + + if modifiers == {"alt"}: + # clear all states + del rto_history["holdout_all"][view_layer] + clear_copy("holdout") + clear_swap("holdout") + + elif modifiers == {"ctrl"}: + copy_rtos(view_layer, "holdout") + + elif modifiers == {"ctrl", "alt"}: + swap_rtos(view_layer, "holdout") + + elif modifiers == {"shift"}: + invert_rtos(view_layer, "holdout") + + else: + activate_all_rtos(view_layer, "holdout") + + return {'FINISHED'} + + +class CMIndirectOnlyOperator(Operator): + bl_label = "[IO] Indirect Only" + bl_description = ( + " * Shift+LMB - Isolate/Restore.\n" + " * Shift+Ctrl+LMB - Isolate nested/Restore.\n" + " * Ctrl+LMB - Toggle nested.\n" + " * Alt+LMB - Discard history" + ) + bl_idname = "view3d.indirect_only_collection" + bl_options = {'REGISTER', 'UNDO'} + + name: StringProperty() + + # static class var + isolated = False + + def invoke(self, context, event): + global rto_history + cls = CMIndirectOnlyOperator + + modifiers = get_modifiers(event) + view_layer = context.view_layer.name + laycol_ptr = layer_collections[self.name]["ptr"] + + if not view_layer in rto_history["indirect"]: + rto_history["indirect"][view_layer] = {"target": "", "history": []} + + + if modifiers == {"alt"}: + del rto_history["indirect"][view_layer] + cls.isolated = False + + elif modifiers == {"shift"}: + isolate_rto(cls, self, view_layer, "indirect") + + elif modifiers == {"ctrl"}: + toggle_children(self, view_layer, "indirect") + + cls.isolated = False + + elif modifiers == {"ctrl", "shift"}: + isolate_rto(cls, self, view_layer, "indirect", children=True) + + else: + # toggle indirect only + + # reset indirect history + del rto_history["indirect"][view_layer] + + # toggle indirect only of collection + laycol_ptr.indirect_only = not laycol_ptr.indirect_only + + cls.isolated = False + + # reset indirect all history + if view_layer in rto_history["indirect_all"]: + del rto_history["indirect_all"][view_layer] + + return {'FINISHED'} + + +class CMUnIndirectOnlyAllOperator(Operator): + bl_label = "[IO Global] Indirect Only" + bl_description = ( + " * LMB - Enable all/Restore.\n" + " * Shift+LMB - Invert.\n" + " * Ctrl+LMB - Copy/Paste RTOs.\n" + " * Ctrl+Alt+LMB - Swap RTOs.\n" + " * Alt+LMB - Discard history" + ) + bl_idname = "view3d.un_indirect_only_all_collections" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + global rto_history + + view_layer = context.view_layer.name + modifiers = get_modifiers(event) + + if not view_layer in rto_history["indirect_all"]: + rto_history["indirect_all"][view_layer] = [] + + if modifiers == {"alt"}: + # clear all states + del rto_history["indirect_all"][view_layer] + clear_copy("indirect") + clear_swap("indirect") + + elif modifiers == {"ctrl"}: + copy_rtos(view_layer, "indirect") + + elif modifiers == {"ctrl", "alt"}: + swap_rtos(view_layer, "indirect") + + elif modifiers == {"shift"}: + invert_rtos(view_layer, "indirect") + + else: + activate_all_rtos(view_layer, "indirect") + + return {'FINISHED'} + + class CMRemoveCollectionOperator(Operator): '''Remove Collection''' bl_label = "Remove Collection" @@ -1112,6 +1317,8 @@ class CMPhantomModeOperator(Operator): "hide": layer_collection.hide_viewport, "disable": layer_collection.collection.hide_viewport, "render": layer_collection.collection.hide_render, + "holdout": layer_collection.holdout, + "indirect": layer_collection.indirect_only, } apply_to_children(view_layer.layer_collection, save_visibility_state) @@ -1132,6 +1339,8 @@ class CMPhantomModeOperator(Operator): layer_collection.hide_viewport = phantom_laycol["hide"] layer_collection.collection.hide_viewport = phantom_laycol["disable"] layer_collection.collection.hide_render = phantom_laycol["render"] + layer_collection.holdout = phantom_laycol["holdout"] + layer_collection.indirect_only = phantom_laycol["indirect"] apply_to_children(view_layer.layer_collection, restore_visibility_state) diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 7858e5bf..0f2703cb 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -302,6 +302,44 @@ class CollectionManager(Operator): global_rto_row.operator("view3d.un_disable_render_all_collections", text="", icon=icon, depress=depress) + if cm.show_holdout: + holdout_all_history = rto_history["holdout_all"].get(view_layer.name, []) + depress = True if len(holdout_all_history) else False + icon = 'HOLDOUT_ON' + buffers = [False, False] + + if copy_buffer["RTO"] == "holdout": + icon = copy_icon + buffers[0] = True + + if swap_buffer["A"]["RTO"] == "holdout": + icon = swap_icon + buffers[1] = True + + if buffers[0] and buffers[1]: + icon = copy_swap_icon + + global_rto_row.operator("view3d.un_holdout_all_collections", text="", icon=icon, depress=depress) + + if cm.show_indirect_only: + indirect_all_history = rto_history["indirect_all"].get(view_layer.name, []) + depress = True if len(indirect_all_history) else False + icon = 'INDIRECT_ONLY_ON' + buffers = [False, False] + + if copy_buffer["RTO"] == "indirect": + icon = copy_icon + buffers[0] = True + + if swap_buffer["A"]["RTO"] == "indirect": + icon = swap_icon + buffers[1] = True + + if buffers[0] and buffers[1]: + icon = copy_swap_icon + + global_rto_row.operator("view3d.un_indirect_only_all_collections", text="", icon=icon, depress=depress) + # treeview layout.row().template_list("CM_UL_items", "", cm, "cm_list_collection", @@ -381,7 +419,7 @@ class CollectionManager(Operator): else: - for rto in ["exclude", "select", "hide", "disable", "render"]: + for rto in ["exclude", "select", "hide", "disable", "render", "holdout", "indirect"]: if new_state[rto] != collection_state[rto]: if view_layer.name in rto_history[rto]: del rto_history[rto][view_layer.name] @@ -628,6 +666,32 @@ class CM_UL_items(UIList): emboss=highlight, depress=highlight) prop.name = item.name + if cm.show_holdout: + holdout_history_base = rto_history["holdout"].get(view_layer.name, {}) + holdout_target = holdout_history_base.get("target", "") + holdout_history = holdout_history_base.get("history", []) + + highlight = bool(holdout_history and holdout_target == item.name) + icon = ('HOLDOUT_ON' if laycol["ptr"].holdout else + 'HOLDOUT_OFF') + + prop = row.operator("view3d.holdout_collection", text="", icon=icon, + emboss=highlight, depress=highlight) + prop.name = item.name + + if cm.show_indirect_only: + indirect_history_base = rto_history["indirect"].get(view_layer.name, {}) + indirect_target = indirect_history_base.get("target", "") + indirect_history = indirect_history_base.get("history", []) + + highlight = bool(indirect_history and indirect_target == item.name) + icon = ('INDIRECT_ONLY_ON' if laycol["ptr"].indirect_only else + 'INDIRECT_ONLY_OFF') + + prop = row.operator("view3d.indirect_only_collection", text="", icon=icon, + emboss=highlight, depress=highlight) + prop.name = item.name + row = s2 @@ -751,6 +815,8 @@ class CMDisplayOptionsPanel(Panel): row.prop(cm, "show_hide_viewport", icon='HIDE_OFF', icon_only=True) row.prop(cm, "show_disable_viewport", icon='RESTRICT_VIEW_OFF', icon_only=True) row.prop(cm, "show_render", icon='RESTRICT_RENDER_OFF', icon_only=True) + row.prop(cm, "show_holdout", icon='HOLDOUT_ON', icon_only=True) + row.prop(cm, "show_indirect_only", icon='INDIRECT_ONLY_ON', icon_only=True) layout.separator() -- cgit v1.2.3 From 14bbd42ff2abb7c480b373a88817095bf8c2ad4c Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 11 Aug 2020 00:51:28 -0400 Subject: Collection Manager: Refactor. Task: T69577 Change detection of the master collection from an ambiguous index to an is_master_collection boolean. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/operator_utils.py | 4 ++-- object_collection_manager/operators.py | 18 +++++++++--------- object_collection_manager/qcd_operators.py | 4 ++-- object_collection_manager/ui.py | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index d7b312a5..e5f6a9ec 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 14, 0), + "version": (2, 14, 1), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel diff --git a/object_collection_manager/operator_utils.py b/object_collection_manager/operator_utils.py index 6f7ee83b..20c7dee7 100644 --- a/object_collection_manager/operator_utils.py +++ b/object_collection_manager/operator_utils.py @@ -469,8 +469,8 @@ def remove_collection(laycol, collection, context): cm.cm_list_index = laycol["row_index"] -def select_collection_objects(collection_index, collection_name, replace, nested): - if collection_index == 0: +def select_collection_objects(is_master_collection, collection_name, replace, nested): + if is_master_collection: target_collection = bpy.context.view_layer.layer_collection.collection else: diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index ff1bbf95..1e265e52 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -74,11 +74,11 @@ class SetActiveCollection(Operator): bl_idname = "view3d.set_active_collection" bl_options = {'UNDO'} - collection_index: IntProperty() + is_master_collection: BoolProperty() collection_name: StringProperty() def execute(self, context): - if self.collection_index == -1: + if self.is_master_collection: layer_collection = context.view_layer.layer_collection else: @@ -227,7 +227,7 @@ class CMSelectCollectionObjectsOperator(Operator): bl_idname = "view3d.select_collection_objects" bl_options = {'REGISTER', 'UNDO'} - collection_index: IntProperty() + is_master_collection: BoolProperty() collection_name: StringProperty() def invoke(self, context, event): @@ -235,7 +235,7 @@ class CMSelectCollectionObjectsOperator(Operator): if modifiers == {"shift"}: select_collection_objects( - collection_index=self.collection_index, + is_master_collection=self.is_master_collection, collection_name=self.collection_name, replace=False, nested=False @@ -243,7 +243,7 @@ class CMSelectCollectionObjectsOperator(Operator): elif modifiers == {"ctrl"}: select_collection_objects( - collection_index=self.collection_index, + is_master_collection=self.is_master_collection, collection_name=self.collection_name, replace=True, nested=True @@ -251,7 +251,7 @@ class CMSelectCollectionObjectsOperator(Operator): elif modifiers == {"ctrl", "shift"}: select_collection_objects( - collection_index=self.collection_index, + is_master_collection=self.is_master_collection, collection_name=self.collection_name, replace=False, nested=True @@ -259,7 +259,7 @@ class CMSelectCollectionObjectsOperator(Operator): else: select_collection_objects( - collection_index=self.collection_index, + is_master_collection=self.is_master_collection, collection_name=self.collection_name, replace=True, nested=False @@ -277,11 +277,11 @@ class CMSetCollectionOperator(Operator): bl_idname = "view3d.set_collection" bl_options = {'REGISTER', 'UNDO'} - collection_index: IntProperty() + is_master_collection: BoolProperty() collection_name: StringProperty() def invoke(self, context, event): - if self.collection_index == 0: + if self.is_master_collection: target_collection = context.view_layer.layer_collection.collection else: diff --git a/object_collection_manager/qcd_operators.py b/object_collection_manager/qcd_operators.py index 4ffec562..b64c87f8 100644 --- a/object_collection_manager/qcd_operators.py +++ b/object_collection_manager/qcd_operators.py @@ -166,7 +166,7 @@ class ViewMoveQCDSlot(Operator): elif modifiers == {"alt"}: select_collection_objects( - collection_index=None, + is_master_collection=False, collection_name=qcd_slots.get_name(self.slot), replace=True, nested=False @@ -174,7 +174,7 @@ class ViewMoveQCDSlot(Operator): elif modifiers == {"alt", "shift"}: select_collection_objects( - collection_index=None, + is_master_collection=False, collection_name=qcd_slots.get_name(self.slot), replace=False, nested=False diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 0f2703cb..4c1eb077 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -159,7 +159,7 @@ class CollectionManager(Operator): prop = c_icon.operator("view3d.set_active_collection", text='', icon='GROUP', depress=highlight) - prop.collection_index = -1 + prop.is_master_collection = True prop.collection_name = 'Master Collection' master_collection_row.separator() @@ -200,7 +200,7 @@ class CollectionManager(Operator): prop = row_setcol.operator("view3d.set_collection", text="", icon=icon, emboss=False) - prop.collection_index = 0 + prop.is_master_collection = True prop.collection_name = 'Master Collection' copy_icon = 'COPYDOWN' @@ -559,7 +559,7 @@ class CM_UL_items(UIList): prop = c_icon.operator("view3d.set_active_collection", text='', icon='GROUP', emboss=highlight, depress=highlight) - prop.collection_index = laycol["row_index"] + prop.is_master_collection = False prop.collection_name = item.name if prefs.enable_qcd: @@ -599,7 +599,7 @@ class CM_UL_items(UIList): prop = set_obj_col.operator("view3d.set_collection", text="", icon=icon, emboss=False) - prop.collection_index = laycol["id"] + prop.is_master_collection = False prop.collection_name = item.name -- cgit v1.2.3 From abeef11a77ab5b05f4ce2c71b65c341bdcb7303d Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Tue, 11 Aug 2020 13:04:26 +0200 Subject: Copy Attributes: Use new constraints.copy() which works better with Armature constraints Currently when copying Armature constraints with the Copy Attributes addon, it does not copy the targets. rB64a584b38a73d4745e introduced a new constraints.copy() function to the Python API which handles this correctly and elegantly with just one line of code. Reviewed By: sybren Differential Revision: https://developer.blender.org/D8422 --- space_view3d_copy_attributes.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/space_view3d_copy_attributes.py b/space_view3d_copy_attributes.py index 30393a75..da741737 100644 --- a/space_view3d_copy_attributes.py +++ b/space_view3d_copy_attributes.py @@ -272,11 +272,7 @@ class CopySelectedPoseConstraints(Operator): for bone in selected: for index, flag in enumerate(self.selection): if flag: - old_constraint = active.constraints[index] - new_constraint = bone.constraints.new( - active.constraints[index].type - ) - generic_copy(old_constraint, new_constraint) + bone.constraints.copy(active.constraints[index]) return {'FINISHED'} -- cgit v1.2.3