From 65bad4212eb91b45e82da82be1ca71b3dcd5f16e Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Wed, 11 Mar 2020 06:41:49 +0100 Subject: glTF exporter: new feature: KHR_materials_clearcoat export --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_gather_image.py | 4 ++ .../blender/exp/gltf2_blender_gather_materials.py | 62 +++++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 49f6f470..c42cbc75 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, 2, 38), + "version": (1, 2, 39), 'blender': (2, 82, 7), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py index c389ba19..d1579803 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py @@ -174,6 +174,10 @@ def __get_image_data(sockets_or_slots, export_settings) -> ExportImage: dst_chan = Channel.R elif socket.name == 'Alpha' and len(sockets_or_slots) > 1 and sockets_or_slots[1] is not None: dst_chan = Channel.A + elif socket.name == 'Clearcoat': + dst_chan = Channel.R + elif socket.name == 'Clearcoat Roughness': + dst_chan = Channel.G if dst_chan is not None: composed_image.fill_image(result.shader_node.image, dst_chan, src_chan) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py index 35028e5b..42c71150 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py @@ -129,10 +129,18 @@ def __gather_emissive_texture(blender_material, export_settings): def __gather_extensions(blender_material, export_settings): extensions = {} + # KHR_materials_unlit + if gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Background") is not None: extensions["KHR_materials_unlit"] = Extension("KHR_materials_unlit", {}, False) - # TODO specular glossiness extension + # KHR_materials_clearcoat + + clearcoat_extension = __gather_clearcoat_extension(blender_material, export_settings) + if clearcoat_extension: + extensions["KHR_materials_clearcoat"] = clearcoat_extension + + # TODO KHR_materials_pbrSpecularGlossiness return extensions if extensions else None @@ -217,3 +225,55 @@ def __has_image_node_from_socket(socket): if not result: return False return True + +def __gather_clearcoat_extension(blender_material, export_settings): + clearcoat_enabled = False + has_clearcoat_texture = False + has_clearcoat_roughness_texture = False + + clearcoat_extension = {} + clearcoat_roughness_slots = () + + clearcoat_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, 'Clearcoat') + clearcoat_roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, 'Clearcoat Roughness') + clearcoat_normal_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, 'Clearcoat Normal') + + if isinstance(clearcoat_socket, bpy.types.NodeSocket) and not clearcoat_socket.is_linked: + clearcoat_extension['clearcoatFactor'] = clearcoat_socket.default_value + clearcoat_enabled = clearcoat_extension['clearcoatFactor'] > 0 + elif __has_image_node_from_socket(clearcoat_socket): + clearcoat_extension['clearcoatFactor'] = 1 + has_clearcoat_texture = True + clearcoat_enabled = True + + if not clearcoat_enabled: + return None + + if isinstance(clearcoat_roughness_socket, bpy.types.NodeSocket) and not clearcoat_roughness_socket.is_linked: + clearcoat_extension['clearcoatRoughnessFactor'] = clearcoat_roughness_socket.default_value + elif __has_image_node_from_socket(clearcoat_roughness_socket): + clearcoat_extension['clearcoatRoughnessFactor'] = 1 + has_clearcoat_roughness_texture = True + + # Pack clearcoat (R) and clearcoatRoughness (G) channels. + if has_clearcoat_texture and has_clearcoat_roughness_texture: + clearcoat_roughness_slots = (clearcoat_socket, clearcoat_roughness_socket,) + elif has_clearcoat_texture: + clearcoat_roughness_slots = (clearcoat_socket,) + elif has_clearcoat_roughness_texture: + clearcoat_roughness_slots = (clearcoat_roughness_socket,) + + if len(clearcoat_roughness_slots) > 0: + combined_texture = gltf2_blender_gather_texture_info.gather_texture_info(clearcoat_roughness_slots, export_settings) + if has_clearcoat_texture: + clearcoat_extension['clearcoatTexture'] = combined_texture + if has_clearcoat_roughness_texture: + clearcoat_extension['clearcoatRoughnessTexture'] = combined_texture + + if __has_image_node_from_socket(clearcoat_normal_socket): + clearcoat_extension['clearcoatNormalTexture'] = gltf2_blender_gather_material_normal_texture_info_class.gather_material_normal_texture_info_class( + (clearcoat_normal_socket,), + export_settings + ) + + return Extension('KHR_materials_clearcoat', clearcoat_extension, False) -- cgit v1.2.3 From e4269fc795cfbadf058a6315474cfc1443091922 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Wed, 11 Mar 2020 06:45:07 +0100 Subject: glTF importer: bugfix when scene has no nodes --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/imp/gltf2_blender_scene.py | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index c42cbc75..61ab3dc3 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, 2, 39), + "version": (1, 2, 40), 'blender': (2, 82, 7), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py index 3c2a8619..691ced91 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py @@ -77,17 +77,27 @@ class BlenderScene(): """Make the first root object from the default glTF scene active. If no default scene, use the first scene, or just any root object. """ - if gltf.data.scenes: - pyscene = gltf.data.scenes[gltf.data.scene or 0] - vnode = gltf.vnodes[pyscene.nodes[0]] - if gltf.vnodes[vnode.parent].type != VNode.DummyRoot: - vnode = gltf.vnodes[vnode.parent] + vnode = None - else: + if gltf.data.scene is not None: + pyscene = gltf.data.scenes[gltf.data.scene] + if pyscene.nodes: + vnode = gltf.vnodes[pyscene.nodes[0]] + + if not vnode: + for pyscene in gltf.data.scenes or []: + if pyscene.nodes: + vnode = gltf.vnodes[pyscene.nodes[0]] + break + + if not vnode: vnode = gltf.vnodes['root'] if vnode.type == VNode.DummyRoot: if not vnode.children: return # no nodes vnode = gltf.vnodes[vnode.children[0]] + if vnode.type == VNode.Bone: + vnode = gltf.vnodes[vnode.bone_arma] + bpy.context.view_layer.objects.active = vnode.blender_object -- cgit v1.2.3 From 520b6af2be38066566ae52781de14e510a0ee987 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Wed, 11 Mar 2020 06:46:55 +0100 Subject: glTF importer: fix bug on windows, use absolute path --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/imp/gltf2_blender_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 61ab3dc3..40e40158 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, 2, 40), + "version": (1, 2, 41), 'blender': (2, 82, 7), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_image.py b/io_scene_gltf2/blender/imp/gltf2_blender_image.py index 0075b7f4..92eed8d4 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_image.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_image.py @@ -58,7 +58,7 @@ class BlenderImage(): path = tmp_file.name num_images = len(bpy.data.images) - blender_image = bpy.data.images.load(path, check_existing=img_from_file) + blender_image = bpy.data.images.load(os.path.abspath(path), check_existing=img_from_file) if len(bpy.data.images) != num_images: # If created a new image blender_image.name = img_name if gltf.import_settings['import_pack_images'] or not img_from_file: -- cgit v1.2.3 From cc237ba4df1bd0ebacbb80086f0cd2f522df1688 Mon Sep 17 00:00:00 2001 From: "Vladimir Spivak(cwolf3d)" Date: Thu, 12 Mar 2020 01:45:16 +0200 Subject: Fix T74551: Switching "Class" setting after add one mesh from "Geodesic Dome" addon, Blender have instant closing --- add_mesh_geodesic_domes/__init__.py | 2 +- add_mesh_geodesic_domes/third_domes_panel_271.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/add_mesh_geodesic_domes/__init__.py b/add_mesh_geodesic_domes/__init__.py index c7137614..00ee10aa 100644 --- a/add_mesh_geodesic_domes/__init__.py +++ b/add_mesh_geodesic_domes/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Geodesic Domes", "author": "Andy Houston", - "version": (0, 3, 5), + "version": (0, 3, 6), "blender": (2, 80, 0), "location": "View3D > Add > Mesh", "description": "Create geodesic dome type objects.", diff --git a/add_mesh_geodesic_domes/third_domes_panel_271.py b/add_mesh_geodesic_domes/third_domes_panel_271.py index f4a9ce54..98add45f 100644 --- a/add_mesh_geodesic_domes/third_domes_panel_271.py +++ b/add_mesh_geodesic_domes/third_domes_panel_271.py @@ -127,7 +127,7 @@ class GenerateGeodesicDome(Operator, object_utils.AddObjectHelper): ("Parabola", "Parabola", "Generate Parabola"), ("Torus", "Torus", "Generate Torus"), ("Sphere", "Sphere", "Generate Sphere"), - ("Import your mesh", "Import your mesh", "Import Your Mesh"), + ("Import_your_mesh", "Import your mesh", "Import Your Mesh"), ], default='Geodesic' ) @@ -157,10 +157,10 @@ class GenerateGeodesicDome(Operator, object_utils.AddObjectHelper): geodesic_class: EnumProperty( name="Class", description="Subdivide Basic/Triacon", - items=[("Class 1", "Class 1", "class one"), - ("Class 2", "Class 2", "class two"), + items=[("Class_1", "Class 1", "class one"), + ("Class_2", "Class 2", "class two"), ], - default='Class 1' + default='Class_1' ) tri_hex_star: EnumProperty( name="Shape", @@ -930,7 +930,7 @@ class GenerateGeodesicDome(Operator, object_utils.AddObjectHelper): row.prop(self, "grxsz") row = layout.row() row.prop(self, "grysz") - elif tmp == 'Import your mesh': + elif tmp == 'Import_your_mesh': col.prop(self, "use_imported_mesh") col.prop(self, "import_mesh_name") # superform parameters only where possible @@ -939,7 +939,7 @@ class GenerateGeodesicDome(Operator, object_utils.AddObjectHelper): row = layout.row() row.prop(self, "vact") row = layout.row() - if not(tmp == 'Import your mesh'): + if tmp != 'Import_your_mesh': if (self.uact is False) and (self.vact is False): row.label(text="No checkbox active", icon="INFO") else: @@ -1117,7 +1117,7 @@ class GenerateGeodesicDome(Operator, object_utils.AddObjectHelper): faceshape = 2 tmp_cl = self.geodesic_class klass = 0 - if tmp_cl == "Class 2": + if tmp_cl == "Class_2": klass = 1 shape = 0 parameters = [self.frequency, self.eccentricity, self.squish, @@ -1167,7 +1167,7 @@ class GenerateGeodesicDome(Operator, object_utils.AddObjectHelper): self.bvellipse, superformparam ) mesh = vefm_271.vefm_add_object(basegeodesic) - elif self.geodesic_types == "Import your mesh": + elif self.geodesic_types == "Import_your_mesh": obj_name = self.import_mesh_name if obj_name == "None": message = "Fill in a name \nof an existing mesh\nto be imported" -- cgit v1.2.3 From bd54740ed08be078a870fcb3d83c7bd4ad304d43 Mon Sep 17 00:00:00 2001 From: "Vladimir Spivak(cwolf3d)" Date: Thu, 12 Mar 2020 04:10:42 +0200 Subject: Fix T74493 and D7045. Redesign. --- curve_tools/__init__.py | 394 +++++++++++++++++++++---------------------- curve_tools/cad.py | 18 +- curve_tools/curves.py | 6 +- curve_tools/internal.py | 40 ++++- curve_tools/intersections.py | 6 +- curve_tools/operators.py | 149 +++++++++++++++- curve_tools/toolpath.py | 125 +++++++++++--- 7 files changed, 501 insertions(+), 237 deletions(-) diff --git a/curve_tools/__init__.py b/curve_tools/__init__.py index 2fcec1d9..4a9d283c 100644 --- a/curve_tools/__init__.py +++ b/curve_tools/__init__.py @@ -25,7 +25,7 @@ bl_info = { "name": "Curve Tools", "description": "Adds some functionality for bezier/nurbs curve/surface modeling", "author": "Mackraken", - "version": (0, 4, 3), + "version": (0, 4, 4), "blender": (2, 80, 0), "location": "View3D > Tool Shelf > Edit Tab", "warning": "WIP", @@ -128,13 +128,13 @@ class curvetoolsSettings(PropertyGroup): description="Only join splines at the starting point of one and the ending point of the other" ) splineJoinModeItems = ( - ('At midpoint', 'At midpoint', 'Join splines at midpoint of neighbouring points'), - ('Insert segment', 'Insert segment', 'Insert segment between neighbouring points') + ('At_midpoint', 'At midpoint', 'Join splines at midpoint of neighbouring points'), + ('Insert_segment', 'Insert segment', 'Insert segment between neighbouring points') ) SplineJoinMode: EnumProperty( items=splineJoinModeItems, name="SplineJoinMode", - default='At midpoint', + default='At_midpoint', description="Determines how the splines will be joined" ) # curve intersection @@ -147,7 +147,7 @@ class curvetoolsSettings(PropertyGroup): intAlgorithmItems = ( ('3D', '3D', 'Detect where curves intersect in 3D'), - ('From View', 'From View', 'Detect where curves intersect in the RegionView3D') + ('From_View', 'From View', 'Detect where curves intersect in the RegionView3D') ) IntersectCurvesAlgorithm: EnumProperty( items=intAlgorithmItems, @@ -229,227 +229,223 @@ class curvetoolsSettings(PropertyGroup): ) -class VIEW3D_PT_CurvePanel(Panel): - bl_label = "Curve Tools" +# Curve Info +class VIEW3D_PT_curve_tools_info(Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" + bl_category = "Curve Edit" + bl_label = "Curve Info" bl_options = {'DEFAULT_CLOSED'} - bl_category = "Edit" - @classmethod - def poll(cls, context): - return context.scene is not None + def draw(self, context): + scene = context.scene + layout = self.layout + + col = layout.column(align=True) + col.operator("curvetools.operatorcurveinfo", text="Curve") + row = col.row(align=True) + row.operator("curvetools.operatorsplinesinfo", text="Spline") + row.operator("curvetools.operatorsegmentsinfo", text="Segment") + row = col.row(align=True) + row.operator("curvetools.operatorcurvelength", icon = "DRIVER_DISTANCE", text="Length") + row.prop(context.scene.curvetools, "CurveLength", text="") + +# Curve Edit +class VIEW3D_PT_curve_tools_edit(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Curve Edit" + bl_label = "Curve Edit" + def draw(self, context): scene = context.scene - SINGLEDROP = scene.UTSingleDrop - MOREDROP = scene.UTMOREDROP - LOFTDROP = scene.UTLoftDrop - ADVANCEDDROP = scene.UTAdvancedDrop - EXTENDEDDROP = scene.UTExtendedDrop - UTILSDROP = scene.UTUtilsDrop layout = self.layout - # Single Curve options - box1 = self.layout.box() - col = box1.column(align=True) + col = layout.column(align=True) + col.operator("curvetools.bezier_points_fillet", text='Fillet/Chamfer') row = col.row(align=True) - row.prop(scene, "UTSingleDrop", icon="TRIA_DOWN") - if SINGLEDROP: - # A. 1 curve - row = col.row(align=True) - - # A.1 curve info/length - row.operator("curvetools.operatorcurveinfo", text="Curve info") - row = col.row(align=True) - row.operator("curvetools.operatorcurvelength", text="Calc Length") - row.prop(context.scene.curvetools, "CurveLength", text="") - - # A.2 splines info - row = col.row(align=True) - row.operator("curvetools.operatorsplinesinfo", text="Curve splines info") - - # A.3 segments info - row = col.row(align=True) - row.operator("curvetools.operatorsegmentsinfo", text="Curve segments info") - - # A.4 origin to spline0start - row = col.row(align=True) - row.operator("curvetools.operatororigintospline0start", text="Set origin to spline start") - - # Double Curve options - box2 = self.layout.box() - col = box2.column(align=True) + row.operator("curvetools.outline", text="Outline") + row.operator("curvetools.add_toolpath_offset_curve", text="Recursive Offset") + col.operator("curvetools.sep_outline", text="Separate Offset/Selected") + col.operator("curvetools.bezier_cad_handle_projection", text='Extend Handles') + col.operator("curvetools.bezier_cad_boolean", text="Boolean Splines") row = col.row(align=True) - row.prop(scene, "UTMOREDROP", icon="TRIA_DOWN") + row.operator("curvetools.bezier_spline_divide", text='Subdivide') + row.operator("curvetools.bezier_cad_subdivide", text="Multi Subdivide") + + col.operator("curvetools.split", text='Split at Vertex') + col.operator("curvetools.add_toolpath_discretize_curve", text="Discretize Curve") + col.operator("curvetools.bezier_cad_array", text="Array Splines") + +# Curve Intersect +class VIEW3D_PT_curve_tools_intersect(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Curve Edit" + bl_label = "Intersect" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + scene = context.scene + layout = self.layout + + col = layout.column(align=True) + col.operator("curvetools.bezier_curve_boolean", text="2D Curve Boolean") + col.operator("curvetools.operatorintersectcurves", text="Intersect Curves") + col.prop(context.scene.curvetools, "LimitDistance", text="Limit Distance") + col.prop(context.scene.curvetools, "IntersectCurvesAlgorithm", text="Algorithm") + col.prop(context.scene.curvetools, "IntersectCurvesMode", text="Mode") + col.prop(context.scene.curvetools, "IntersectCurvesAffect", text="Affect") - if MOREDROP: - # B. 2 curves - row = col.row(align=True) +# Curve Surfaces +class VIEW3D_PT_curve_tools_surfaces(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Curve Edit" + bl_label = "Surfaces" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + wm = context.window_manager + scene = context.scene + layout = self.layout - # B.1 curve intersections - row = col.row(align=True) - row.operator("curvetools.operatorintersectcurves", text="Intersect curves") + col = layout.column(align=True) + col.operator("curvetools.operatorbirail", text="Birail") + col.operator("curvetools.convert_bezier_to_surface", text="Convert Bezier to Surface") + col.operator("curvetools.convert_selected_face_to_bezier", text="Convert Faces to Bezier") - row = col.row(align=True) - row.prop(context.scene.curvetools, "LimitDistance", text="LimitDistance") +# Curve Path Finder +class VIEW3D_PT_curve_tools_loft(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Curve Edit" + bl_parent_id = "VIEW3D_PT_curve_tools_surfaces" + bl_label = "Loft" + bl_options = {'DEFAULT_CLOSED'} - row = col.row(align=True) - row.prop(context.scene.curvetools, "IntersectCurvesAlgorithm", text="Algorithm") + def draw(self, context): + wm = context.window_manager + scene = context.scene + layout = self.layout - row = col.row(align=True) - row.prop(context.scene.curvetools, "IntersectCurvesMode", text="Mode") + col = layout.column(align=True) + col.operator("curvetools.create_auto_loft") + lofters = [o for o in scene.objects if "autoloft" in o.keys()] + for o in lofters: + col.label(text=o.name) + # layout.prop(o, '["autoloft"]', toggle=True) + col.prop(wm, "auto_loft", toggle=True) + col.operator("curvetools.update_auto_loft_curves") + col = layout.column(align=True) - row = col.row(align=True) - row.prop(context.scene.curvetools, "IntersectCurvesAffect", text="Affect") - # Loft options - box1 = self.layout.box() - col = box1.column(align=True) +# Curve Sanitize +class VIEW3D_PT_curve_tools_sanitize(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Curve Edit" + bl_label = "Sanitize" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + scene = context.scene + layout = self.layout + + col = layout.column(align=True) + col.operator("curvetools.operatororigintospline0start", icon = "OBJECT_ORIGIN", text="Set Origin to Spline Start") + col.operator("curvetools.scale_reset", text='Reset Scale') + + col.label(text="Cleanup:") + col.operator("curvetools.remove_doubles", icon = "TRASH", text='Remove Doubles') + col.operator("curvetools.operatorsplinesremovezerosegment", icon = "TRASH", text="0-Segment Splines") + row = col.row(align=True) + row.operator("curvetools.operatorsplinesremoveshort", text="Short Splines") + row.prop(context.scene.curvetools, "SplineRemoveLength", text="Threshold remove") + + col.label(text="Join Splines:") + col.operator("curvetools.operatorsplinesjoinneighbouring", text="Join Neighbouring Splines") row = col.row(align=True) - row.prop(scene, "UTLoftDrop", icon="TRIA_DOWN") - - if LOFTDROP: - # B.2 surface generation - wm = context.window_manager - scene = context.scene - layout = self.layout - layout.operator("curvetools.create_auto_loft") - lofters = [o for o in scene.objects if "autoloft" in o.keys()] - for o in lofters: - layout.label(text=o.name) - # layout.prop(o, '["autoloft"]', toggle=True) - layout.prop(wm, "auto_loft", toggle=True) - layout.operator("curvetools.update_auto_loft_curves") - - # Advanced options - box1 = self.layout.box() - col = box1.column(align=True) + col.prop(context.scene.curvetools, "SplineJoinDistance", text="Threshold") + col.prop(context.scene.curvetools, "SplineJoinStartEnd", text="Only at Ends") + col.prop(context.scene.curvetools, "SplineJoinMode", text="Join Position") + +# Curve Utilities +class VIEW3D_PT_curve_tools_utilities(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Curve Edit" + bl_label = "Utilities" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + scene = context.scene + layout = self.layout + + col = layout.column(align=True) row = col.row(align=True) - row.prop(scene, "UTAdvancedDrop", icon="TRIA_DOWN") - if ADVANCEDDROP: - # C. 3 curves - row = col.row(align=True) - row.operator("curvetools.outline", text="Curve Outline") - row = col.row(align=True) - row.operator("curvetools.sep_outline", text="Separate Outline or selected") - row = col.row(align=True) - row.operator("curvetools.bezier_curve_boolean", text="2D Curve Boolean") - row = col.row(align=True) - row.operator("curvetools.bezier_points_fillet", text='Fillet') - row = col.row(align=True) - row.operator("curvetools.bezier_cad_handle_projection", text='Handle Projection') - row = col.row(align=True) - row.operator("curvetools.bezier_spline_divide", text='Divide') - row = col.row(align=True) - row.operator("curvetools.scale_reset", text='Scale Reset') - row = col.row(align=True) - row.operator("curvetools.operatorbirail", text="Birail") - row = col.row(align=True) - row.operator("curvetools.convert_selected_face_to_bezier", text="Convert selected faces to Bezier") - row = col.row(align=True) - row.operator("curvetools.convert_bezier_to_surface", text="Convert Bezier to Surface") - - # Extended options - box1 = self.layout.box() - col = box1.column(align=True) + row.label(text="Curve Resolution:") row = col.row(align=True) - row.prop(scene, "UTExtendedDrop", icon="TRIA_DOWN") - if EXTENDEDDROP: - row = col.row(align=True) - row.operator("curvetools.add_toolpath_offset_curve", text="Offset Curve") - row = col.row(align=True) - row.operator("curvetools.bezier_cad_boolean", text="Boolean 2 selected spline") - row = col.row(align=True) - row.operator("curvetools.bezier_cad_subdivide", text="Multi Subdivide") - row = col.row(align=True) - row.operator("curvetools.split", text='Split by selected points') - row = col.row(align=True) - row.operator("curvetools.remove_doubles", text='Remove Doubles') - row = col.row(align=True) - row.operator("curvetools.add_toolpath_discretize_curve", text="Discretize Curve") - row = col.row(align=True) - row.operator("curvetools.bezier_cad_array", text="Array selected spline") - - # Utils Curve options - box1 = self.layout.box() - col = box1.column(align=True) + row.operator("curvetools.show_resolution", icon="HIDE_OFF", text="Show [ESC]") + row.prop(context.scene.curvetools, "curve_vertcolor", text="") row = col.row(align=True) - row.prop(scene, "UTUtilsDrop", icon="TRIA_DOWN") - if UTILSDROP: - # D.1 set spline resolution - row = col.row(align=True) - row.label(text="Show point Resolution:") - row = col.row(align=True) - row.operator("curvetools.operatorsplinessetresolution", text="Set resolution") - row.prop(context.scene.curvetools, "SplineResolution", text="") - row = col.row(align=True) - row.prop(context.scene.curvetools, "curve_vertcolor", text="") - row = col.row(align=True) - row.operator("curvetools.show_resolution", text="Run [ESC]") - - # D.1 set spline sequence - row = col.row(align=True) - row.label(text="Show and rearrange spline sequence:") - row = col.row(align=True) - row.prop(context.scene.curvetools, "sequence_color", text="") - row.prop(context.scene.curvetools, "font_thickness", text="") - row.prop(context.scene.curvetools, "font_size", text="") - row = col.row(align=True) - oper = row.operator("curvetools.rearrange_spline", text="<") - oper.command = 'PREV' - oper = row.operator("curvetools.rearrange_spline", text=">") - oper.command = 'NEXT' - row = col.row(align=True) - row.operator("curvetools.show_splines_sequence", text="Run [ESC]") - - # D.2 remove splines - row = col.row(align=True) - row.label(text="Remove splines:") - row = col.row(align=True) - row.operator("curvetools.operatorsplinesremovezerosegment", text="Remove 0-segments splines") - row = col.row(align=True) - row.operator("curvetools.operatorsplinesremoveshort", text="Remove short splines") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineRemoveLength", text="Threshold remove") - - # D.3 join splines - row = col.row(align=True) - row.label(text="Join splines:") - row = col.row(align=True) - row.operator("curvetools.operatorsplinesjoinneighbouring", text="Join neighbouring splines") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineJoinDistance", text="Threshold join") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineJoinStartEnd", text="Only at start & end") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineJoinMode", text="Join mode") - - row = col.row(align=True) - row.label(text="PathFinder:") - row = col.row(align=True) - row.prop(context.scene.curvetools, "PathFinderRadius", text="PathFinder Radius") - row = col.row(align=True) - row.prop(context.scene.curvetools, "path_color", text="") - row.prop(context.scene.curvetools, "path_thickness", text="") - row = col.row(align=True) - row.operator("curvetools.pathfinder", text="Run Path Finder [ESC]") - row = col.row(align=True) - row.label(text="ESC or TAB - exit from PathFinder") - row = col.row(align=True) - row.label(text="X or DEL - delete") - row = col.row(align=True) - row.label(text="Alt + mouse click - select spline") - row = col.row(align=True) - row.label(text="Alt + Shift + mouse click - add spline to select") - row = col.row(align=True) - row.label(text="A - deselect all") + row.operator("curvetools.operatorsplinessetresolution", text="Set Resolution") + row.prop(context.scene.curvetools, "SplineResolution", text="") + + + row = col.row(align=True) + row.label(text="Spline Order:") + row = col.row(align=True) + row.operator("curvetools.show_splines_sequence", icon="HIDE_OFF", text="Show [ESC]") + row.prop(context.scene.curvetools, "sequence_color", text="") + row = col.row(align=True) + row.prop(context.scene.curvetools, "font_size", text="Font Size") + row.prop(context.scene.curvetools, "font_thickness", text="Font Thickness") + row = col.row(align=True) + oper = row.operator("curvetools.rearrange_spline", text = "<") + oper.command = 'PREV' + oper = row.operator("curvetools.rearrange_spline", text = ">") + oper.command = 'NEXT' + row = col.row(align=True) + row.operator("curve.switch_direction", text="Switch Direction") + row = col.row(align=True) + row.operator("curvetools.set_first_points", text="Set First Points") + +# Curve Path Finder +class VIEW3D_PT_curve_tools_pathfinder(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Curve Edit" + bl_parent_id = "VIEW3D_PT_curve_tools_utilities" + bl_label = "Path Finder" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + scene = context.scene + layout = self.layout + + col = layout.column(align=True) + col.operator("curvetools.pathfinder", text="Path Finder [ESC]") + col.prop(context.scene.curvetools, "PathFinderRadius", text="PathFinder Radius") + col.prop(context.scene.curvetools, "path_color", text="") + col.prop(context.scene.curvetools, "path_thickness", text="Thickness") + + col = layout.column(align=True) + col.label(text="ESC or TAB - Exit PathFinder") + col.label(text="X or DEL - Delete") + col.label(text="Alt + Mouse Click - Select Spline") + col.label(text="Alt + Shift + Mouse click - Add Spline to Selection") + col.label(text="A - Deselect All") # Add-ons Preferences Update Panel # Define Panel classes for updating panels = ( - VIEW3D_PT_CurvePanel, + VIEW3D_PT_curve_tools_info, VIEW3D_PT_curve_tools_edit, + VIEW3D_PT_curve_tools_intersect, VIEW3D_PT_curve_tools_surfaces, + VIEW3D_PT_curve_tools_loft, VIEW3D_PT_curve_tools_sanitize, + VIEW3D_PT_curve_tools_utilities, VIEW3D_PT_curve_tools_pathfinder ) diff --git a/curve_tools/cad.py b/curve_tools/cad.py index 3d3f87bd..e46af8d3 100644 --- a/curve_tools/cad.py +++ b/curve_tools/cad.py @@ -156,10 +156,14 @@ class MergeEnds(bpy.types.Operator): self.report({'WARNING'}, 'Invalid selection') return {'CANCELLED'} + if is_last_point[0]: + points[1], points[0] = points[0], points[1] + selected_splines[1], selected_splines[0] = selected_splines[0], selected_splines[1] + is_last_point[1], is_last_point[0] = is_last_point[0], is_last_point[1] + points[0].handle_left_type = 'FREE' points[0].handle_right_type = 'FREE' new_co = (points[0].co+points[1].co)*0.5 - handle = (points[1].handle_left if is_last_point[1] else points[1].handle_right)+new_co-points[1].co if is_last_point[0]: points[0].handle_left += new_co-points[0].co @@ -169,13 +173,13 @@ class MergeEnds(bpy.types.Operator): points[0].handle_left = handle points[0].co = new_co - bpy.ops.curve.select_all(action='DESELECT') - points[1].select_control_point = True - bpy.ops.curve.delete() - selected_splines[0].bezier_points[-1 if is_last_point[0] else 0].select_control_point = True - selected_splines[1].bezier_points[-1 if is_last_point[1] else 0].select_control_point = True + point_index = 0 if selected_splines[0] == selected_splines[1] else len(selected_splines[1].bezier_points) bpy.ops.curve.make_segment() - bpy.ops.curve.select_all(action='DESELECT') + point = selected_splines[0].bezier_points[point_index] + point.select_control_point = False + point.select_left_handle = False + point.select_right_handle = False + bpy.ops.curve.delete() return {'FINISHED'} class Subdivide(bpy.types.Operator): diff --git a/curve_tools/curves.py b/curve_tools/curves.py index da0b1398..202487de 100644 --- a/curve_tools/curves.py +++ b/curve_tools/curves.py @@ -357,12 +357,12 @@ class BezierSpline: return [newSpline1, newSpline2] - def Join(self, spline2, mode = 'At midpoint'): - if mode == 'At midpoint': + def Join(self, spline2, mode = 'At_midpoint'): + if mode == 'At_midpoint': self.JoinAtMidpoint(spline2) return - if mode == 'Insert segment': + if mode == 'Insert_segment': self.JoinInsertSegment(spline2) return diff --git a/curve_tools/internal.py b/curve_tools/internal.py index e967fc6e..96816189 100644 --- a/curve_tools/internal.py +++ b/curve_tools/internal.py @@ -103,11 +103,11 @@ def nearestPointOfLines(originA, dirA, originB, dirB, tollerance=0.0): def lineSegmentLineSegmentIntersection(beginA, endA, beginB, endB, tollerance=0.001): dirA = endA-beginA dirB = endB-beginB - intersection = nearestPointOfLines(beginA, dirA, beginB, dirB) - if math.isnan(intersection[0]) or (intersection[2]-intersection[3]).length > tollerance or \ - intersection[0] < 0 or intersection[0] > 1 or intersection[1] < 0 or intersection[1] > 1: + paramA, paramB, pointA, pointB = nearestPointOfLines(beginA, dirA, beginB, dirB) + if math.isnan(paramA) or (pointA-pointB).length > tollerance or \ + paramA < 0 or paramA > 1 or paramB < 0 or paramB > 1: return None - return intersection + return (paramA, paramB, pointA, pointB) def aabbOfPoints(points): min = Vector(points[0]) @@ -290,6 +290,16 @@ def isSegmentLinear(points, tollerance=0.0001): def bezierSegmentPoints(begin, end): return [begin.co, begin.handle_right, end.handle_left, end.co] +def grab_cursor(context, event): + if event.mouse_region_x < 0: + context.window.cursor_warp(context.region.x+context.region.width, event.mouse_y) + elif event.mouse_region_x > context.region.width: + context.window.cursor_warp(context.region.x, event.mouse_y) + elif event.mouse_region_y < 0: + context.window.cursor_warp(event.mouse_x, context.region.y+context.region.height) + elif event.mouse_region_y > context.region.height: + context.window.cursor_warp(event.mouse_x, context.region.y) + def deleteFromArray(item, array): for index, current in enumerate(array): if current is item: @@ -586,7 +596,6 @@ def getSelectedSplines(include_bezier, include_polygon, allow_partial_selection= return result def addObject(type, name): - bpy.ops.object.select_all(action='DESELECT') if type == 'CURVE': data = bpy.data.curves.new(name=name, type='CURVE') data.dimensions = '3D' @@ -780,6 +789,27 @@ def filletSpline(spline, radius, chamfer_mode, limit_half_way, tollerance=0.0001 i = i+1 return addBezierSpline(bpy.context.object, spline.use_cyclic_u, vertices) +def dogBone(spline, radius): + vertices = [] + def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last): + if not selected or is_first or is_last or angle == 0 or normal[2] > 0.0 or \ + (spline.type == 'BEZIER' and not (isSegmentLinear(prev_segment_points) and isSegmentLinear(next_segment_points))): + prev_handle = next_segment_points[0] if is_first else prev_segment_points[2] if spline.type == 'BEZIER' else prev_segment_points[0] + next_handle = next_segment_points[0] if is_last else next_segment_points[1] if spline.type == 'BEZIER' else next_segment_points[3] + vertices.append([prev_handle, next_segment_points[0], next_handle]) + return + tan_factor = math.tan(angle*0.5) + corner = next_segment_points[0]+normal.cross(prev_tangent)*radius-prev_tangent*radius*tan_factor + direction = next_segment_points[0]-corner + distance = direction.length + corner = next_segment_points[0]+direction/distance*(distance-radius) + vertices.append([prev_segment_points[0], next_segment_points[0], corner]) + vertices.append([next_segment_points[0], corner, next_segment_points[0]]) + vertices.append([corner, next_segment_points[0], next_segment_points[3]]) + iterateSpline(spline, handlePoint) + print(vertices) + return vertices + def discretizeCurve(spline, step_angle, samples): vertices = [] def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last): diff --git a/curve_tools/intersections.py b/curve_tools/intersections.py index 77f19861..f0b8e96f 100644 --- a/curve_tools/intersections.py +++ b/curve_tools/intersections.py @@ -29,7 +29,7 @@ class BezierSegmentsIntersector: if algorithm == '3D': return self.CalcFirstRealIntersection3D(nrSamples1, nrSamples2) - if algorithm == 'From View': + if algorithm == 'From_View': global algoDIR if algoDIR is not None: return self.CalcFirstRealIntersectionFromViewDIR(nrSamples1, nrSamples2) @@ -309,7 +309,7 @@ class BezierSegmentsIntersector: if algorithm == '3D': return self.CalcIntersections3D(nrSamples1, nrSamples2) - if algorithm == 'From View': + if algorithm == 'From_View': global algoDIR if algoDIR is not None: return self.CalcIntersectionsFromViewDIR(nrSamples1, nrSamples2) @@ -527,7 +527,7 @@ class CurvesIntersector: global algoDIR algo = bpy.context.scene.curvetools.IntersectCurvesAlgorithm - if algo == 'From View': + if algo == 'From_View': regionView3D = util.GetFirstRegionView3D() if regionView3D is None: print("### ERROR: regionView3D is None. Stopping.") diff --git a/curve_tools/operators.py b/curve_tools/operators.py index aeb4672c..ea11aef3 100644 --- a/curve_tools/operators.py +++ b/curve_tools/operators.py @@ -1145,7 +1145,153 @@ class CurveBoolean(bpy.types.Operator): j += 1 - bpy.ops.object.mode_set (mode = current_mode) + bpy.ops.object.mode_set(mode = 'EDIT') + bpy.ops.curve.select_all(action='SELECT') + + return {'FINISHED'} + +# ---------------------------- +# Set first points operator +class SetFirstPoints(bpy.types.Operator): + bl_idname = "curvetools.set_first_points" + bl_label = "Set first points" + bl_description = "Set the selected points as the first point of each spline" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return util.Selected1OrMoreCurves() + + def execute(self, context): + splines_to_invert = [] + + curve = bpy.context.object + + bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT') + + # Check non-cyclic splines to invert + for i in range(len(curve.data.splines)): + b_points = curve.data.splines[i].bezier_points + + if i not in self.cyclic_splines: # Only for non-cyclic splines + if b_points[len(b_points) - 1].select_control_point: + splines_to_invert.append(i) + + # Reorder points of cyclic splines, and set all handles to "Automatic" + + # Check first selected point + cyclic_splines_new_first_pt = {} + for i in self.cyclic_splines: + sp = curve.data.splines[i] + + for t in range(len(sp.bezier_points)): + bp = sp.bezier_points[t] + if bp.select_control_point or bp.select_right_handle or bp.select_left_handle: + cyclic_splines_new_first_pt[i] = t + break # To take only one if there are more + + # Reorder + for spline_idx in cyclic_splines_new_first_pt: + sp = curve.data.splines[spline_idx] + + spline_old_coords = [] + for bp_old in sp.bezier_points: + coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2]) + + left_handle_type = str(bp_old.handle_left_type) + left_handle_length = float(bp_old.handle_left.length) + left_handle_xyz = ( + float(bp_old.handle_left.x), + float(bp_old.handle_left.y), + float(bp_old.handle_left.z) + ) + right_handle_type = str(bp_old.handle_right_type) + right_handle_length = float(bp_old.handle_right.length) + right_handle_xyz = ( + float(bp_old.handle_right.x), + float(bp_old.handle_right.y), + float(bp_old.handle_right.z) + ) + spline_old_coords.append( + [coords, left_handle_type, + right_handle_type, left_handle_length, + right_handle_length, left_handle_xyz, + right_handle_xyz] + ) + + for t in range(len(sp.bezier_points)): + bp = sp.bezier_points + + if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1: + new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 + else: + new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp) + + bp[t].co = Vector(spline_old_coords[new_index][0]) + + bp[t].handle_left.length = spline_old_coords[new_index][3] + bp[t].handle_right.length = spline_old_coords[new_index][4] + + bp[t].handle_left_type = "FREE" + bp[t].handle_right_type = "FREE" + + bp[t].handle_left.x = spline_old_coords[new_index][5][0] + bp[t].handle_left.y = spline_old_coords[new_index][5][1] + bp[t].handle_left.z = spline_old_coords[new_index][5][2] + + bp[t].handle_right.x = spline_old_coords[new_index][6][0] + bp[t].handle_right.y = spline_old_coords[new_index][6][1] + bp[t].handle_right.z = spline_old_coords[new_index][6][2] + + bp[t].handle_left_type = spline_old_coords[new_index][1] + bp[t].handle_right_type = spline_old_coords[new_index][2] + + # Invert the non-cyclic splines designated above + for i in range(len(splines_to_invert)): + bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT') + + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + + bpy.ops.curve.switch_direction() + + bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT') + + # Keep selected the first vert of each spline + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + for i in range(len(curve.data.splines)): + if not curve.data.splines[i].use_cyclic_u: + bp = curve.data.splines[i].bezier_points[0] + else: + bp = curve.data.splines[i].bezier_points[ + len(curve.data.splines[i].bezier_points) - 1 + ] + + bp.select_control_point = True + bp.select_right_handle = True + bp.select_left_handle = True + + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + + return {'FINISHED'} + + def invoke(self, context, event): + curve = bpy.context.object + + # Check if all curves are Bezier, and detect which ones are cyclic + self.cyclic_splines = [] + for i in range(len(curve.data.splines)): + if curve.data.splines[i].type != "BEZIER": + self.report({'WARNING'}, "All splines must be Bezier type") + + return {'CANCELLED'} + else: + if curve.data.splines[i].use_cyclic_u: + self.cyclic_splines.append(i) + + self.execute(context) + self.report({'INFO'}, "First points have been set") return {'FINISHED'} @@ -1182,4 +1328,5 @@ operators = [ Split, SeparateOutline, CurveBoolean, + SetFirstPoints, ] diff --git a/curve_tools/toolpath.py b/curve_tools/toolpath.py index 2b422280..fec6693a 100644 --- a/curve_tools/toolpath.py +++ b/curve_tools/toolpath.py @@ -17,6 +17,7 @@ # ***** GPL LICENSE BLOCK ***** import bpy, math, bmesh +from bpy_extras import view3d_utils from mathutils import Vector, Matrix from . import internal @@ -68,28 +69,19 @@ class SliceMesh(bpy.types.Operator): bl_description = bl_label = 'Slice Mesh' bl_options = {'REGISTER', 'UNDO'} - pitch_axis: bpy.props.FloatVectorProperty(name='Pitch & Axis', unit='LENGTH', description='Vector between to slices', subtype='DIRECTION', default=(0.0, 0.0, 0.1), size=3) - offset: bpy.props.FloatProperty(name='Offset', unit='LENGTH', description='Position of first slice along axis', default=-0.4) - slice_count: bpy.props.IntProperty(name='Count', description='Number of slices', min=1, default=9) + pitch: bpy.props.FloatProperty(name='Pitch', unit='LENGTH', description='Distance between two slices', default=0.1) + offset: bpy.props.FloatProperty(name='Offset', unit='LENGTH', description='Position of first slice along the axis', default=0.0) + slice_count: bpy.props.IntProperty(name='Count', description='Number of slices', min=1, default=3) @classmethod def poll(cls, context): return bpy.context.object != None and bpy.context.object.mode == 'OBJECT' - def execute(self, context): - if bpy.context.object.type != 'MESH': - self.report({'WARNING'}, 'Active object must be a mesh') - return {'CANCELLED'} - depsgraph = context.evaluated_depsgraph_get() - mesh = bmesh.new() - mesh.from_object(bpy.context.object, depsgraph, deform=True, cage=False, face_normals=True) - mesh.transform(bpy.context.object.matrix_world) - toolpath = internal.addObject('CURVE', 'Slices Toolpath') - pitch_axis = Vector(self.pitch_axis) - axis = pitch_axis.normalized() + def perform(self, context): + axis = Vector((0.0, 0.0, 1.0)) for i in range(0, self.slice_count): - aux_mesh = mesh.copy() - cut_geometry = bmesh.ops.bisect_plane(aux_mesh, geom=aux_mesh.edges[:]+aux_mesh.faces[:], dist=0, plane_co=pitch_axis*i+axis*self.offset, plane_no=axis, clear_outer=False, clear_inner=False)['geom_cut'] + aux_mesh = self.mesh.copy() + cut_geometry = bmesh.ops.bisect_plane(aux_mesh, geom=aux_mesh.edges[:]+aux_mesh.faces[:], dist=0, plane_co=axis*(i*self.pitch+self.offset), plane_no=axis, clear_outer=False, clear_inner=False)['geom_cut'] edge_pool = set([e for e in cut_geometry if isinstance(e, bmesh.types.BMEdge)]) while len(edge_pool) > 0: current_edge = edge_pool.pop() @@ -110,9 +102,104 @@ class SliceMesh(bpy.types.Operator): break current_vertex = current_edge.other_vert(current_vertex) vertices.append(current_vertex.co) - internal.addPolygonSpline(toolpath, False, vertices) + internal.addPolygonSpline(self.result, False, vertices) aux_mesh.free() - mesh.free() + + def invoke(self, context, event): + if bpy.context.object.type != 'MESH': + self.report({'WARNING'}, 'Active object must be a mesh') + return {'CANCELLED'} + self.pitch = 0.1 + self.offset = 0.0 + self.slice_count = 3 + self.mode = 'PITCH' + self.execute(context) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + if event.type == 'MOUSEMOVE': + mouse = (event.mouse_region_x, event.mouse_region_y) + input_value = internal.nearestPointOfLines( + bpy.context.scene.cursor.location, + bpy.context.scene.cursor.matrix.col[2].xyz, + view3d_utils.region_2d_to_origin_3d(context.region, context.region_data, mouse), + view3d_utils.region_2d_to_vector_3d(context.region, context.region_data, mouse) + )[0] + if self.mode == 'PITCH': + self.pitch = input_value/(self.slice_count-1) if self.slice_count > 2 else input_value + elif self.mode == 'OFFSET': + self.offset = input_value-self.pitch*0.5*((self.slice_count-1) if self.slice_count > 2 else 1.0) + elif event.type == 'WHEELUPMOUSE': + if self.slice_count > 2: + self.pitch *= (self.slice_count-1) + self.slice_count += 1 + if self.slice_count > 2: + self.pitch /= (self.slice_count-1) + elif event.type == 'WHEELDOWNMOUSE': + if self.slice_count > 2: + self.pitch *= (self.slice_count-1) + if self.slice_count > 1: + self.slice_count -= 1 + if self.slice_count > 2: + self.pitch /= (self.slice_count-1) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + if self.mode == 'PITCH': + self.mode = 'OFFSET' + return {'RUNNING_MODAL'} + elif self.mode == 'OFFSET': + return {'FINISHED'} + elif event.type in {'RIGHTMOUSE', 'ESC'}: + bpy.context.scene.collection.objects.unlink(self.result) + return {'CANCELLED'} + else: + return {'PASS_THROUGH'} + self.result.data.splines.clear() + self.perform(context) + return {'RUNNING_MODAL'} + + def execute(self, context): + depsgraph = context.evaluated_depsgraph_get() + self.mesh = bmesh.new() + self.mesh.from_object(bpy.context.object, depsgraph, deform=True, cage=False, face_normals=True) + self.mesh.transform(bpy.context.scene.cursor.matrix.inverted()@bpy.context.object.matrix_world) + self.result = internal.addObject('CURVE', 'Slices') + self.result.matrix_world = bpy.context.scene.cursor.matrix + self.perform(context) + return {'FINISHED'} + +class DogBone(bpy.types.Operator): + bl_idname = 'curvetools.add_toolpath_dogbone' + bl_description = bl_label = 'Dog Bone' + bl_options = {'REGISTER', 'UNDO'} + + radius: bpy.props.FloatProperty(name='Radius', description='Tool radius to compensate for', unit='LENGTH', min=0.0, default=0.1) + + @classmethod + def poll(cls, context): + return bpy.context.object != None and bpy.context.object.type == 'CURVE' + + def execute(self, context): + if bpy.context.object.mode == 'EDIT': + splines = internal.getSelectedSplines(True, False) + else: + splines = bpy.context.object.data.splines + + if len(splines) == 0: + self.report({'WARNING'}, 'Nothing selected') + return {'CANCELLED'} + + if bpy.context.object.mode != 'EDIT': + internal.addObject('CURVE', 'Dog Bone') + origin = bpy.context.scene.cursor.location + else: + origin = Vector((0.0, 0.0, 0.0)) + + for spline in splines: + if spline.type != 'BEZIER': + continue + result = internal.dogBone(spline, self.radius) + internal.addBezierSpline(bpy.context.object, spline.use_cyclic_u, result) # [vertex-origin for vertex in result]) return {'FINISHED'} class DiscretizeCurve(bpy.types.Operator): @@ -295,4 +382,4 @@ def unregister(): if __name__ == "__main__": register() -operators = [OffsetCurve, SliceMesh, DiscretizeCurve, Truncate, RectMacro, DrillMacro] +operators = [OffsetCurve, SliceMesh, DogBone, DiscretizeCurve, Truncate, RectMacro, DrillMacro] -- cgit v1.2.3 From 1ae12e6f5d3c131b0b3e390a7c5e8cdb992d21e5 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Thu, 12 Mar 2020 21:59:04 +0100 Subject: glTF exporter: manage user extension at gltf level --- io_scene_gltf2/__init__.py | 2 +- io_scene_gltf2/blender/exp/gltf2_blender_export.py | 5 +++++ io_scene_gltf2/io/exp/gltf2_io_user_extensions.py | 13 +++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 40e40158..e60bf8b8 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, 2, 41), + "version": (1, 2, 42), 'blender': (2, 82, 7), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py index 0e415e7a..6d9ab8bb 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_export.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py @@ -24,6 +24,7 @@ from io_scene_gltf2.blender.exp.gltf2_blender_gltf2_exporter import GlTF2Exporte from io_scene_gltf2.io.com.gltf2_io_debug import print_console, print_newline from io_scene_gltf2.io.exp import gltf2_io_export from io_scene_gltf2.io.exp import gltf2_io_draco_compression_extension +from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions def save(context, export_settings): @@ -61,6 +62,10 @@ def __export(export_settings): def __gather_gltf(exporter, export_settings): active_scene_idx, scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings) + plan = {'active_scene_idx': active_scene_idx, 'scenes': scenes, 'animations': animations} + export_user_extensions('gather_gltf_hook', export_settings, plan) + active_scene_idx, scenes, animations = plan['active_scene_idx'], plan['scenes'], plan['animations'] + if export_settings['gltf_draco_mesh_compression']: gltf2_io_draco_compression_extension.compress_scene_primitives(scenes, export_settings) exporter.add_draco_extension() diff --git a/io_scene_gltf2/io/exp/gltf2_io_user_extensions.py b/io_scene_gltf2/io/exp/gltf2_io_user_extensions.py index 424713c3..a149673f 100644 --- a/io_scene_gltf2/io/exp/gltf2_io_user_extensions.py +++ b/io_scene_gltf2/io/exp/gltf2_io_user_extensions.py @@ -12,11 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -def export_user_extensions(hook_name, export_settings, gltf2_object, *args): - if gltf2_object.extensions is None: - gltf2_object.extensions = {} +def export_user_extensions(hook_name, export_settings, *args): + if args and hasattr(args[0], "extensions"): + if args[0].extensions is None: + args[0].extensions = {} for extension in export_settings['gltf_user_extensions']: hook = getattr(extension, hook_name, None) if hook is not None: - hook(gltf2_object, *args, export_settings) + try: + hook(*args, export_settings) + except Exception as e: + print(hook_name, "fails on", extension) + print(str(e)) -- cgit v1.2.3 From 1d722cb79a597ab1c7da55071a8bb08ca6874de4 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Thu, 12 Mar 2020 19:38:56 -0400 Subject: Collection Manager: Add isolate tree feature. Task: T69577 Switches the current hotkey for expanding/collapsing all sublevels from shift-click to ctrl-click. Isolate tree is set to shift-click. --- object_collection_manager/__init__.py | 2 +- object_collection_manager/internals.py | 5 +++- object_collection_manager/operators.py | 46 ++++++++++++++++++++++++++++++++-- object_collection_manager/ui.py | 7 ++++-- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 39a22906..4d895df7 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": (1,9,3), + "version": (1,10,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 5267b1c6..e7f63884 100644 --- a/object_collection_manager/internals.py +++ b/object_collection_manager/internals.py @@ -80,7 +80,10 @@ def update_collection_tree(context): "ptr": layer_collection } - get_all_collections(context, init_laycol_list, master_laycol, collection_tree, visible=True) + get_all_collections(context, init_laycol_list, master_laycol, master_laycol["children"], visible=True) + + for laycol in master_laycol["children"]: + collection_tree.append(laycol) def get_all_collections(context, collections, parent, tree, level=0, visible=False): diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 7f693ac9..473a5908 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -73,8 +73,9 @@ class ExpandAllOperator(Operator): return {'FINISHED'} +expand_history = {"target": "", "history": []} class ExpandSublevelOperator(Operator): - ''' * Shift-Click to expand/collapse all sublevels''' + ''' * Ctrl-Click to expand/collapse all sublevels\n * Shift-Click to isolate/restore tree''' bl_label = "Expand Sublevel Items" bl_idname = "view3d.expand_sublevel" bl_options = {'REGISTER', 'UNDO'} @@ -83,8 +84,16 @@ class ExpandSublevelOperator(Operator): name: StringProperty() index: IntProperty() + # static class var + isolated = False + def invoke(self, context, event): - if event.shift: + global expand_history + cls = ExpandSublevelOperator + + modifiers = get_modifiers(event) + + if modifiers == {"ctrl"}: # expand/collapse all subcollections expand = None @@ -111,6 +120,35 @@ class ExpandSublevelOperator(Operator): loop(layer_collections[self.name]["ptr"]) + expand_history["target"] = "" + expand_history["history"].clear() + cls.isolated = False + + elif modifiers == {"shift"}: + def isolate_tree(current_laycol): + parent = current_laycol["parent"] + + for laycol in parent["children"]: + if laycol["name"] != current_laycol["name"] and laycol["name"] in expanded: + expanded.remove(laycol["name"]) + expand_history["history"].append(laycol["name"]) + + if parent["parent"]: + isolate_tree(parent) + + if cls.isolated: + for item in expand_history["history"]: + expanded.append(item) + + expand_history["target"] = "" + expand_history["history"].clear() + cls.isolated = False + + else: + isolate_tree(layer_collections[self.name]) + expand_history["target"] = self.name + cls.isolated = True + else: # expand/collapse collection if self.expand: @@ -118,6 +156,10 @@ class ExpandSublevelOperator(Operator): else: expanded.remove(self.name) + expand_history["target"] = "" + expand_history["history"].clear() + cls.isolated = False + # set selected row to the collection you're expanding/collapsing and update tree view context.scene.collection_manager.cm_list_index = self.index diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index dcd804fa..88e9d0cc 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -37,6 +37,7 @@ from .internals import ( from .operators import ( rto_history, + expand_history, phantom_history, ) @@ -277,8 +278,10 @@ class CM_UL_items(UIList): # add expander if collection has children to make UIList act like tree view if laycol["has_children"]: if laycol["expanded"]: - prop = row.operator("view3d.expand_sublevel", text="", - icon='DISCLOSURE_TRI_DOWN', emboss=False) + highlight = True if expand_history["target"] == item.name else False + + prop = row.operator("view3d.expand_sublevel", text="", icon='DISCLOSURE_TRI_DOWN', + emboss=highlight, depress=highlight) prop.expand = False prop.name = item.name prop.index = index -- cgit v1.2.3 From a35d66c1337ae5e3f86f11a760ad24770386f9a9 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Sun, 15 Mar 2020 07:49:40 +0100 Subject: glTF exporter: rename option to use_selection, like other exporter options --- io_scene_gltf2/__init__.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index e60bf8b8..46a8ccda 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, 2, 42), + "version": (1, 2, 43), 'blender': (2, 82, 7), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -222,7 +222,7 @@ class ExportGLTF2_Base: default=False ) - use_selected: BoolProperty( + use_selection: BoolProperty( name='Selected Objects', description='Export selected objects only', default=False @@ -352,11 +352,11 @@ class ExportGLTF2_Base: if settings: try: for (k, v) in settings.items(): - if k == "export_selected": # Back compatibility for export_selected --> use_selected - setattr(self, "use_selected", v) + if k == "export_selected": # Back compatibility for export_selected --> use_selection + setattr(self, "use_selection", v) del settings[k] - settings["use_selected"] = v - print("export_selected is now renamed use_selected, and will be deleted in a few release") + settings["use_selection"] = v + print("export_selected is now renamed use_selection, and will be deleted in a few release") else: setattr(self, k, v) self.will_save_settings = True @@ -431,14 +431,14 @@ class ExportGLTF2_Base: export_settings['gltf_colors'] = self.export_colors export_settings['gltf_cameras'] = self.export_cameras - # compatibility after renaming export_selected to use_selected + # compatibility after renaming export_selected to use_selection if self.export_selected is True: - self.report({"WARNING"}, "export_selected is now renamed use_selected, and will be deleted in a few release") + self.report({"WARNING"}, "export_selected is now renamed use_selection, and will be deleted in a few release") export_settings['gltf_selected'] = self.export_selected else: - export_settings['gltf_selected'] = self.use_selected + export_settings['gltf_selected'] = self.use_selection - # export_settings['gltf_selected'] = self.use_selected This can be uncomment when removing compatibility of export_selected + # export_settings['gltf_selected'] = self.use_selection This can be uncomment when removing compatibility of export_selected export_settings['gltf_layers'] = True # self.export_layers export_settings['gltf_extras'] = self.export_extras export_settings['gltf_yup'] = self.export_yup @@ -556,7 +556,7 @@ class GLTF_PT_export_include(bpy.types.Panel): sfile = context.space_data operator = sfile.active_operator - layout.prop(operator, 'use_selected') + layout.prop(operator, 'use_selection') layout.prop(operator, 'export_extras') layout.prop(operator, 'export_cameras') layout.prop(operator, 'export_lights') -- cgit v1.2.3 From b752de9e0da4e8ad694de25497275f66168a2df4 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Mon, 16 Mar 2020 02:48:02 -0400 Subject: Collection Manager: Add QCD System. Task: T69577 Adds a Quick Content Display (QCD) system to the Collection Manager. This consists of a 3D View Header widget and a floating panel similar to the layers system in blender 2.7x, along with hotkeys to view/move objects to QCD slots, and additions to the main Collection Manager popup to allow you to manage which collections correspond to which slots. --- object_collection_manager/__init__.py | 118 +++- object_collection_manager/icons/minus.png | Bin 0 -> 1934 bytes object_collection_manager/internals.py | 195 +++++- object_collection_manager/operators.py | 9 + object_collection_manager/preferences.py | 512 ++++++++++++++ object_collection_manager/qcd_move_widget.py | 969 +++++++++++++++++++++++++++ object_collection_manager/qcd_operators.py | 286 ++++++++ object_collection_manager/ui.py | 246 +++++-- 8 files changed, 2277 insertions(+), 58 deletions(-) create mode 100644 object_collection_manager/icons/minus.png create mode 100644 object_collection_manager/preferences.py create mode 100644 object_collection_manager/qcd_move_widget.py create mode 100644 object_collection_manager/qcd_operators.py diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 4d895df7..37bf9c3a 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": (1,10,0), + "version": (2,0,0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -36,19 +36,30 @@ if "bpy" in locals(): importlib.reload(internals) importlib.reload(operators) + importlib.reload(preferences) + importlib.reload(qcd_move_widget) + importlib.reload(qcd_operators) importlib.reload(ui) else: from . import internals from . import operators + from . import preferences + from . import qcd_move_widget + from . import qcd_operators from . import ui +import os import bpy +import bpy.utils.previews +from bpy.app.handlers import persistent from bpy.types import PropertyGroup from bpy.props import ( CollectionProperty, + EnumProperty, IntProperty, BoolProperty, + StringProperty, PointerProperty, ) @@ -65,6 +76,10 @@ class CollectionManagerProperties(PropertyGroup): in_phantom_mode: BoolProperty(default=False) + update_header: CollectionProperty(type=internals.CMListCollection) + + qcd_slots_blend_data: StringProperty() + addon_keymaps = [] @@ -87,30 +102,131 @@ classes = ( operators.CMRemoveCollectionOperator, operators.CMSetCollectionOperator, operators.CMPhantomModeOperator, + preferences.CMPreferences, + qcd_move_widget.QCDMoveWidget, + qcd_operators.MoveToQCDSlot, + qcd_operators.ViewQCDSlot, + qcd_operators.ViewMoveQCDSlot, + qcd_operators.RenumerateQCDSlots, ui.CM_UL_items, ui.CollectionManager, ui.CMRestrictionTogglesPanel, CollectionManagerProperties, ) +@persistent +def depsgraph_update_post_handler(dummy): + if qcd_operators.move_triggered: + qcd_operators.move_triggered = False + return + + qcd_operators.move_selection.clear() + qcd_operators.move_active = None + qcd_operators.get_move_selection() + qcd_operators.get_move_active() + +@persistent +def save_internal_data(dummy): + cm = bpy.context.scene.collection_manager + + cm.qcd_slots_blend_data = internals.qcd_slots.get_data_for_blend() + +@persistent +def load_internal_data(dummy): + cm = bpy.context.scene.collection_manager + data = cm.qcd_slots_blend_data + + if not data: + return + + internals.qcd_slots.load_blend_data(data) + def register(): for cls in classes: bpy.utils.register_class(cls) + + pcoll = bpy.utils.previews.new() + icons_dir = os.path.join(os.path.dirname(__file__), "icons") + pcoll.load("active_icon_base", os.path.join(icons_dir, "minus.png"), 'IMAGE', True) + pcoll.load("active_icon_text", os.path.join(icons_dir, "minus.png"), 'IMAGE', True) + pcoll.load("active_icon_text_sel", os.path.join(icons_dir, "minus.png"), 'IMAGE', True) + ui.preview_collections["icons"] = pcoll + + bpy.types.Scene.collection_manager = PointerProperty(type=CollectionManagerProperties) + bpy.types.VIEW3D_HT_header.append(ui.view3d_header_qcd_slots) + # create the global menu hotkey wm = bpy.context.window_manager km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') kmi = km.keymap_items.new('view3d.collection_manager', 'M', 'PRESS') addon_keymaps.append((km, kmi)) + # create qcd hotkeys + qcd_hotkeys = [ + ["ONE", False, "1"], + ["TWO", False, "2"], + ["THREE", False, "3"], + ["FOUR", False, "4"], + ["FIVE", False, "5"], + ["SIX", False, "6"], + ["SEVEN", False, "7"], + ["EIGHT", False, "8"], + ["NINE", False, "9"], + ["ZERO", False, "10"], + ["ONE", True, "11"], + ["TWO", True, "12"], + ["THREE", True, "13"], + ["FOUR", True, "14"], + ["FIVE", True, "15"], + ["SIX", True, "16"], + ["SEVEN", True, "17"], + ["EIGHT", True, "18"], + ["NINE", True, "19"], + ["ZERO", True, "20"], + ] + + for key in qcd_hotkeys: + km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') + kmi = km.keymap_items.new('view3d.view_qcd_slot', key[0], 'PRESS', alt=key[1]) + kmi.properties.slot = key[2] + kmi.properties.toggle = False + addon_keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') + kmi = km.keymap_items.new('view3d.view_qcd_slot', key[0], 'PRESS',shift=True, alt=key[1]) + kmi.properties.slot = key[2] + kmi.properties.toggle = True + addon_keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') + kmi = km.keymap_items.new('view3d.qcd_move_widget', 'V', 'PRESS') + addon_keymaps.append((km, kmi)) + + bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post_handler) + bpy.app.handlers.save_pre.append(save_internal_data) + bpy.app.handlers.load_post.append(load_internal_data) + def unregister(): + bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post_handler) + bpy.app.handlers.save_pre.remove(save_internal_data) + bpy.app.handlers.load_post.remove(load_internal_data) + for cls in classes: bpy.utils.unregister_class(cls) + for pcoll in ui.preview_collections.values(): + bpy.utils.previews.remove(pcoll) + ui.preview_collections.clear() + ui.last_icon_theme_text = None + ui.last_icon_theme_text_sel = None + del bpy.types.Scene.collection_manager + bpy.types.VIEW3D_HT_header.remove(ui.view3d_header_qcd_slots) + # remove keymaps when add-on is deactivated for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) diff --git a/object_collection_manager/icons/minus.png b/object_collection_manager/icons/minus.png new file mode 100644 index 00000000..dff25acd Binary files /dev/null and b/object_collection_manager/icons/minus.png differ diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py index e7f63884..5ebc6025 100644 --- a/object_collection_manager/internals.py +++ b/object_collection_manager/internals.py @@ -25,46 +25,183 @@ from bpy.types import ( Operator, ) -from bpy.props import StringProperty +from bpy.props import ( + StringProperty, + IntProperty, +) layer_collections = {} - collection_tree = [] - expanded = [] - -max_lvl = 0 row_index = 0 +max_lvl = 0 def get_max_lvl(): return max_lvl + +class QCDSlots(): + _slots = {} + overrides = {} + allow_update = True + + def __iter__(self): + return self._slots.items().__iter__() + + def __repr__(self): + return self._slots.__repr__() + + def __contains__(self, key): + try: + int(key) + return key in self._slots + + except ValueError: + return key in self._slots.values() + + return False + + def get_data_for_blend(self): + return f"{self._slots.__repr__()}\n{self.overrides.__repr__()}" + + def load_blend_data(self, data): + decoupled_data = data.split("\n") + blend_slots = eval(decoupled_data[0]) + blend_overrides = eval(decoupled_data[1]) + + self._slots = blend_slots + self.overrides = blend_overrides + + def length(self): + return len(self._slots) + + def get_idx(self, name, r_value=None): + for k, v in self._slots.items(): + if v == name: + return k + + return r_value + + def get_name(self, idx, r_value=None): + if idx in self._slots: + return self._slots[idx] + + return r_value + + def add_slot(self, idx, name): + self._slots[idx] = name + + def update_slot(self, idx, name): + self._slots[idx] = name + + def del_slot(self, slot): + try: + int(slot) + del self._slots[slot] + + except ValueError: + idx = self.get_idx(slot) + del self._slots[idx] + + def clear(self): + self._slots.clear() + +qcd_slots = QCDSlots() + + def update_col_name(self, context): + global layer_collections + global qcd_slots + if self.name != self.last_name: if self.name == '': self.name = self.last_name return if self.last_name != '': + # update collection name layer_collections[self.last_name]["ptr"].collection.name = self.name + # update qcd_slot + idx = qcd_slots.get_idx(self.last_name) + if idx: + qcd_slots.update_slot(idx, self.name) + update_property_group(context) self.last_name = self.name +def update_qcd_slot(self, context): + global qcd_slots + + if not qcd_slots.allow_update: + return + + update_needed = False + + try: + int(self.qcd_slot) + except: + + if self.qcd_slot == "": + qcd_slots.del_slot(self.name) + qcd_slots.overrides[self.name] = True + + if self.name in qcd_slots: + qcd_slots.allow_update = False + self.qcd_slot = qcd_slots.get_idx(self.name) + qcd_slots.allow_update = True + + if self.name in qcd_slots.overrides: + qcd_slots.allow_update = False + self.qcd_slot = "" + qcd_slots.allow_update = True + + return + + if self.name in qcd_slots: + qcd_slots.del_slot(self.name) + update_needed = True + + if self.qcd_slot in qcd_slots: + qcd_slots.overrides[qcd_slots.get_name(self.qcd_slot)] = True + qcd_slots.del_slot(self.qcd_slot) + update_needed = True + + if int(self.qcd_slot) > 20: + self.qcd_slot = "20" + + if int(self.qcd_slot) < 1: + self.qcd_slot = "1" + + qcd_slots.add_slot(self.qcd_slot, self.name) + + if self.name in qcd_slots.overrides: + del qcd_slots.overrides[self.name] + + + if update_needed: + update_property_group(context) + + class CMListCollection(PropertyGroup): name: StringProperty(update=update_col_name) last_name: StringProperty() + qcd_slot: StringProperty(name="QCD Slot", update=update_qcd_slot) -def update_collection_tree(context): +def update_collection_tree(context, renumerate=False): global max_lvl global row_index + global collection_tree + global layer_collections + global qcd_slots + collection_tree.clear() layer_collections.clear() + max_lvl = 0 row_index = 0 - layer_collection = context.view_layer.layer_collection init_laycol_list = layer_collection.children @@ -85,6 +222,37 @@ def update_collection_tree(context): for laycol in master_laycol["children"]: collection_tree.append(laycol) + # update qcd + for x in range(20): + qcd_slot = qcd_slots.get_name(str(x+1)) + if qcd_slot and not layer_collections.get(qcd_slot, None): + qcd_slots.del_slot(qcd_slot) + + # update autonumeration + if qcd_slots.length() < 20: + lvl = 0 + num = 1 + while lvl <= max_lvl: + if num > 20: + break + + for laycol in layer_collections.values(): + if num > 20: + break + + if int(laycol["lvl"]) == lvl: + if laycol["name"] in qcd_slots.overrides: + if not renumerate: + num += 1 + continue + + if str(num) not in qcd_slots and laycol["name"] not in qcd_slots: + qcd_slots.add_slot(str(num), laycol["name"]) + + num += 1 + + lvl += 1 + def get_all_collections(context, collections, parent, tree, level=0, visible=False): global row_index @@ -122,20 +290,29 @@ def get_all_collections(context, collections, parent, tree, level=0, visible=Fal get_all_collections(context, item.children, laycol, laycol["children"], level+1) -def update_property_group(context): - update_collection_tree(context) +def update_property_group(context, renumerate=False): + global collection_tree + global qcd_slots + + qcd_slots.allow_update = False + + update_collection_tree(context, renumerate) context.scene.collection_manager.cm_list_collection.clear() create_property_group(context, collection_tree) + qcd_slots.allow_update = True + def create_property_group(context, tree): global in_filter + global qcd_slots cm = context.scene.collection_manager for laycol in tree: new_cm_listitem = cm.cm_list_collection.add() new_cm_listitem.name = laycol["name"] + new_cm_listitem.qcd_slot = qcd_slots.get_idx(laycol["name"], "") if laycol["has_children"]: create_property_group(context, laycol["children"]) diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 473a5908..067a5277 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -35,6 +35,7 @@ from bpy.props import ( from .internals import ( expanded, layer_collections, + qcd_slots, update_property_group, get_modifiers, send_report, @@ -1418,6 +1419,7 @@ class CMRemoveCollectionOperator(Operator): def execute(self, context): global rto_history + global qcd_slots cm = context.scene.collection_manager @@ -1448,6 +1450,13 @@ class CMRemoveCollectionOperator(Operator): update_property_group(context) + # update qcd + if self.collection_name in qcd_slots: + qcd_slots.del_slot(self.collection_name) + + if self.collection_name in qcd_slots.overrides: + del qcd_slots.overrides[self.collection_name] + # reset history for rto in rto_history.values(): rto.clear() diff --git a/object_collection_manager/preferences.py b/object_collection_manager/preferences.py new file mode 100644 index 00000000..154ee3ee --- /dev/null +++ b/object_collection_manager/preferences.py @@ -0,0 +1,512 @@ +# ##### 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 ##### + +# Copyright 2011, Ryan Inch + +import bpy +from bpy.types import AddonPreferences +from bpy.props import ( + BoolProperty, + FloatProperty, + FloatVectorProperty, + ) + +def get_tool_text(self): + if self.tool_text_override: + return self["tool_text_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.text + self["tool_text_color"] = color.r, color.g, color.b + return self["tool_text_color"] + +def set_tool_text(self, values): + self["tool_text_color"] = values[0], values[1], values[2] + + +def get_tool_text_sel(self): + if self.tool_text_sel_override: + return self["tool_text_sel_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.text_sel + self["tool_text_sel_color"] = color.r, color.g, color.b + return self["tool_text_sel_color"] + +def set_tool_text_sel(self, values): + self["tool_text_sel_color"] = values[0], values[1], values[2] + + +def get_tool_inner(self): + if self.tool_inner_override: + return self["tool_inner_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.inner + self["tool_inner_color"] = color[0], color[1], color[2], color[3] + return self["tool_inner_color"] + +def set_tool_inner(self, values): + self["tool_inner_color"] = values[0], values[1], values[2], values[3] + + +def get_tool_inner_sel(self): + if self.tool_inner_sel_override: + return self["tool_inner_sel_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.inner_sel + self["tool_inner_sel_color"] = color[0], color[1], color[2], color[3] + return self["tool_inner_sel_color"] + +def set_tool_inner_sel(self, values): + self["tool_inner_sel_color"] = values[0], values[1], values[2], values[3] + + +def get_tool_outline(self): + if self.tool_outline_override: + return self["tool_outline_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.outline + self["tool_outline_color"] = color.r, color.g, color.b + return self["tool_outline_color"] + +def set_tool_outline(self, values): + self["tool_outline_color"] = values[0], values[1], values[2] + + +def get_menu_back_text(self): + if self.menu_back_text_override: + return self["menu_back_text_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_menu_back.text + self["menu_back_text_color"] = color.r, color.g, color.b + return self["menu_back_text_color"] + +def set_menu_back_text(self, values): + self["menu_back_text_color"] = values[0], values[1], values[2] + + +def get_menu_back_inner(self): + if self.menu_back_inner_override: + return self["menu_back_inner_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_menu_back.inner + self["menu_back_inner_color"] = color[0], color[1], color[2], color[3] + return self["menu_back_inner_color"] + +def set_menu_back_inner(self, values): + self["menu_back_inner_color"] = values[0], values[1], values[2], values[3] + + +def get_menu_back_outline(self): + if self.menu_back_outline_override: + return self["menu_back_outline_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_menu_back.outline + self["menu_back_outline_color"] = color.r, color.g, color.b + return self["menu_back_outline_color"] + +def set_menu_back_outline(self, values): + self["menu_back_outline_color"] = values[0], values[1], values[2] + + +def get_tooltip_text(self): + if self.tooltip_text_override: + return self["tooltip_text_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.text + self["tooltip_text_color"] = color.r, color.g, color.b + return self["tooltip_text_color"] + +def set_tooltip_text(self, values): + self["tooltip_text_color"] = values[0], values[1], values[2] + + +def get_tooltip_inner(self): + if self.tooltip_inner_override: + return self["tooltip_inner_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.inner + self["tooltip_inner_color"] = color[0], color[1], color[2], color[3] + return self["tooltip_inner_color"] + +def set_tooltip_inner(self, values): + self["tooltip_inner_color"] = values[0], values[1], values[2], values[3] + + +def get_tooltip_outline(self): + if self.tooltip_outline_override: + return self["tooltip_outline_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.outline + self["tooltip_outline_color"] = color.r, color.g, color.b + return self["tooltip_outline_color"] + +def set_tooltip_outline(self, values): + self["tooltip_outline_color"] = values[0], values[1], values[2] + + +class CMPreferences(AddonPreferences): + bl_idname = __package__ + + # OVERRIDE BOOLS + tool_text_override: BoolProperty( + name="Text", + description="Override Theme Text Color", + default=False, + ) + + tool_text_sel_override: BoolProperty( + name="Selection", + description="Override Theme Text Selection Color", + default=False, + ) + + tool_inner_override: BoolProperty( + name="Inner", + description="Override Theme Inner Color", + default=False, + ) + + tool_inner_sel_override: BoolProperty( + name="Selection", + description="Override Theme Inner Selection Color", + default=False, + ) + + tool_outline_override: BoolProperty( + name="Outline", + description="Override Theme Outline Color", + default=False, + ) + + menu_back_text_override: BoolProperty( + name="Text", + description="Override Theme Text Color", + default=False, + ) + + menu_back_inner_override: BoolProperty( + name="Inner", + description="Override Theme Inner Color", + default=False, + ) + + menu_back_outline_override: BoolProperty( + name="Outline", + description="Override Theme Outline Color", + default=False, + ) + + tooltip_text_override: BoolProperty( + name="Text", + description="Override Theme Text Color", + default=False, + ) + + tooltip_inner_override: BoolProperty( + name="Inner", + description="Override Theme Inner Color", + default=False, + ) + + tooltip_outline_override: BoolProperty( + name="Outline", + description="Override Theme Outline Color", + default=False, + ) + + + # OVERRIDE COLORS + qcd_ogl_widget_tool_text: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Text Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.text, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tool_text, + set=set_tool_text, + ) + + qcd_ogl_widget_tool_text_sel: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Text Selection Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.text_sel, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tool_text_sel, + set=set_tool_text_sel, + ) + + qcd_ogl_widget_tool_inner: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Inner Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.inner, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_tool_inner, + set=set_tool_inner, + ) + + qcd_ogl_widget_tool_inner_sel: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Inner Selection Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.inner_sel, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_tool_inner_sel, + set=set_tool_inner_sel, + ) + + qcd_ogl_widget_tool_outline: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Outline Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.outline, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tool_outline, + set=set_tool_outline, + ) + + qcd_ogl_widget_menu_back_text: FloatVectorProperty( + name="", + description="QCD Move Widget Menu Back Text Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_menu_back.text, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_menu_back_text, + set=set_menu_back_text, + ) + + qcd_ogl_widget_menu_back_inner: FloatVectorProperty( + name="", + description="QCD Move Widget Menu Back Inner Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_menu_back.inner, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_menu_back_inner, + set=set_menu_back_inner, + ) + + qcd_ogl_widget_menu_back_outline: FloatVectorProperty( + name="", + description="QCD Move Widget Menu Back Outline Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_menu_back.outline, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_menu_back_outline, + set=set_menu_back_outline, + ) + + qcd_ogl_widget_tooltip_text: FloatVectorProperty( + name="", + description="QCD Move Widget Tooltip Text Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tooltip.text, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tooltip_text, + set=set_tooltip_text, + ) + + qcd_ogl_widget_tooltip_inner: FloatVectorProperty( + name="", + description="QCD Move Widget Tooltip Inner Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tooltip.inner, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_tooltip_inner, + set=set_tooltip_inner, + ) + + qcd_ogl_widget_tooltip_outline: FloatVectorProperty( + name="", + description="QCD Move Widget Tooltip Outline Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tooltip.outline, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tooltip_outline, + set=set_tooltip_outline, + ) + + # NON ACTIVE ICON ALPHA + qcd_ogl_selected_icon_alpha: FloatProperty( + name="Selected Icon Alpha", + description="Set the 'Selected' icon's alpha value", + default=0.9, + min=0.0, + max=1.0, + ) + + qcd_ogl_objects_icon_alpha: FloatProperty( + name="Objects Icon Alpha", + description="Set the 'Objects' icon's alpha value", + default=0.5, + min=0.0, + max=1.0, + ) + + def draw(self, context): + layout = self.layout + box = layout.box() + + box.row().label(text="QCD Move Widget") + + tool_box = box.box() + tool_box.row().label(text="Tool Theme Overrides:") + tool_box.use_property_split = True + + flow = tool_box.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.alignment = 'LEFT' + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "tool_text_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_text_override + row.prop(self, "qcd_ogl_widget_tool_text") + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "tool_text_sel_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_text_sel_override + row.prop(self, "qcd_ogl_widget_tool_text_sel") + + col = flow.column() + col.alignment = 'RIGHT' + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tool_inner_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_inner_override + row.prop(self, "qcd_ogl_widget_tool_inner") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tool_inner_sel_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_inner_sel_override + row.prop(self, "qcd_ogl_widget_tool_inner_sel") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tool_outline_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_outline_override + row.prop(self, "qcd_ogl_widget_tool_outline") + + tool_box.use_property_split = False + tool_box.row().label(text="Icon Alpha:") + icon_fade_row = tool_box.row() + icon_fade_row.alignment = 'EXPAND' + icon_fade_row.prop(self, "qcd_ogl_selected_icon_alpha", text="Selected") + icon_fade_row.prop(self, "qcd_ogl_objects_icon_alpha", text="Objects") + + + menu_back_box = box.box() + menu_back_box.use_property_split = True + menu_back_box.row().label(text="Menu Back Theme Overrides:") + + flow = menu_back_box.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.alignment = 'LEFT' + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "menu_back_text_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.menu_back_text_override + row.prop(self, "qcd_ogl_widget_menu_back_text") + + col = flow.column() + col.alignment = 'RIGHT' + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "menu_back_inner_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.menu_back_inner_override + row.prop(self, "qcd_ogl_widget_menu_back_inner") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "menu_back_outline_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.menu_back_outline_override + row.prop(self, "qcd_ogl_widget_menu_back_outline") + + + tooltip_box = box.box() + tooltip_box.use_property_split = True + tooltip_box.row().label(text="Tooltip Theme Overrides:") + + flow = tooltip_box.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.alignment = 'LEFT' + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "tooltip_text_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tooltip_text_override + row.prop(self, "qcd_ogl_widget_tooltip_text") + + col = flow.column() + col.alignment = 'RIGHT' + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tooltip_inner_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tooltip_inner_override + row.prop(self, "qcd_ogl_widget_tooltip_inner") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tooltip_outline_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tooltip_outline_override + row.prop(self, "qcd_ogl_widget_tooltip_outline") diff --git a/object_collection_manager/qcd_move_widget.py b/object_collection_manager/qcd_move_widget.py new file mode 100644 index 00000000..442d8cfb --- /dev/null +++ b/object_collection_manager/qcd_move_widget.py @@ -0,0 +1,969 @@ +# ##### 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 ##### + +# Copyright 2011, Ryan Inch + +import time +from math import cos, sin, pi, floor +import bpy +import bgl +import blf +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(): + spacer = 10 + return round(spacer * scale_factor()) + +def scale_factor(): + return bpy.context.preferences.system.ui_scale + +def get_coords(area): + x = area["vert"][0] + y = area["vert"][1] + w = area["width"] + h = area["height"] + + vertices = ( + (x, y-h), # bottom left + (x+w, y-h), # bottom right + (x, y), # top left + (x+w, y)) # top right + + indices = ( + (0, 1, 2), (2, 1, 3)) + + return vertices, indices + +def get_x_coords(area): + x = area["vert"][0] + y = area["vert"][1] + w = area["width"] + h = area["height"] + + vertices = ( + (x, y), # top left A + (x+(w*0.1), y), # top left B + (x+w, y), # top right A + (x+w-(w*0.1), y), # top right B + (x, y-h), # bottom left A + (x+(w*0.1), y-h), # bottom left B + (x+w, y-h), # bottom right A + (x+w-(w*0.1), y-h), # bottom right B + (x+(w/2)-(w*0.05), y-(h/2)), # center left + (x+(w/2)+(w*0.05), y-(h/2)) # center right + ) + + indices = ( + (0,1,8), (1,8,9), # top left bar + (2,3,9), (3,9,8), # top right bar + (4,5,8), (5,8,9), # bottom left bar + (6,7,8), (6,9,8) # bottom right bar + ) + + return vertices, indices + +def get_circle_coords(area): + # set x, y to center + x = area["vert"][0] + area["width"] / 2 + y = area["vert"][1] - area["width"] / 2 + radius = area["width"] / 2 + sides = 32 + vertices = [(radius * cos(side * 2 * pi / sides) + x, + radius * sin(side * 2 * pi / sides) + y) + for side in range(sides + 1)] + + return vertices + +def draw_rounded_rect(area, shader, color, tl=5, tr=5, bl=5, br=5, outline=False): + sides = 32 + + tl = round(tl * scale_factor()) + tr = round(tr * scale_factor()) + bl = round(bl * scale_factor()) + br = round(br * scale_factor()) + + bgl.glEnable(bgl.GL_BLEND) + + if outline: + thickness = round(2 * scale_factor()) + thickness = max(thickness, 2) + + bgl.glLineWidth(thickness) + bgl.glEnable(bgl.GL_LINE_SMOOTH) + bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST) + + draw_type = 'TRI_FAN' if not outline else 'LINE_STRIP' + + # top left corner + vert_x = area["vert"][0] + tl + vert_y = area["vert"][1] - tl + tl_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (8<=side<=16): + cosine = tl * cos(side * 2 * pi / sides) + vert_x + sine = tl * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + # top right corner + vert_x = area["vert"][0] + area["width"] - tr + vert_y = area["vert"][1] - tr + tr_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (0<=side<=8): + cosine = tr * cos(side * 2 * pi / sides) + vert_x + sine = tr * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + # bottom left corner + vert_x = area["vert"][0] + bl + vert_y = area["vert"][1] - area["height"] + bl + bl_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (16<=side<=24): + cosine = bl * cos(side * 2 * pi / sides) + vert_x + sine = bl * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + # bottom right corner + vert_x = area["vert"][0] + area["width"] - br + vert_y = area["vert"][1] - area["height"] + br + br_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (24<=side<=32): + cosine = br * cos(side * 2 * pi / sides) + vert_x + sine = br * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + if not outline: + vertices = [] + indices = [] + base_ind = 0 + + # left edge + width = max(tl, bl) + le_x = tl_vert[0]-tl + vertices.extend([ + (le_x, tl_vert[1]), + (le_x+width, tl_vert[1]), + (le_x, bl_vert[1]), + (le_x+width, bl_vert[1]) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # right edge + width = max(tr, br) + re_x = tr_vert[0]+tr + vertices.extend([ + (re_x, tr_vert[1]), + (re_x-width, tr_vert[1]), + (re_x, br_vert[1]), + (re_x-width, br_vert[1]) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # top edge + width = max(tl, tr) + te_y = tl_vert[1]+tl + vertices.extend([ + (tl_vert[0], te_y), + (tl_vert[0], te_y-width), + (tr_vert[0], te_y), + (tr_vert[0], te_y-width) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # bottom edge + width = max(bl, br) + be_y = bl_vert[1]-bl + vertices.extend([ + (bl_vert[0], be_y), + (bl_vert[0], be_y+width), + (br_vert[0], be_y), + (br_vert[0], be_y+width) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # middle + vertices.extend([ + tl_vert, + tr_vert, + bl_vert, + br_vert + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + + batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) + + shader.uniform_float("color", color) + batch.draw(shader) + + else: + overlap = round(thickness / 2 - scale_factor() / 2) + + # left edge + le_x = tl_vert[0]-tl + vertices = [ + (le_x, tl_vert[1] + (overlap if tl == 0 else 0)), + (le_x, bl_vert[1] - (overlap if bl == 0 else 0)) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + # right edge + re_x = tr_vert[0]+tr + vertices = [ + (re_x, tr_vert[1] + (overlap if tr == 0 else 0)), + (re_x, br_vert[1] - (overlap if br == 0 else 0)) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + # top edge + te_y = tl_vert[1]+tl + vertices = [ + (tl_vert[0] - (overlap if tl == 0 else 0), te_y), + (tr_vert[0] + (overlap if tr == 0 else 0), te_y) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + # bottom edge + be_y = bl_vert[1]-bl + vertices = [ + (bl_vert[0] - (overlap if bl == 0 else 0), be_y), + (br_vert[0] + (overlap if br == 0 else 0), be_y) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + bgl.glDisable(bgl.GL_LINE_SMOOTH) + + bgl.glDisable(bgl.GL_BLEND) + +def mouse_in_area(mouse_pos, area, buf = 0): + x = mouse_pos[0] + y = mouse_pos[1] + + # check left + if x+buf < area["vert"][0]: + return False + + # check right + if x-buf > area["vert"][0] + area["width"]: + return False + + # check top + if y-buf > area["vert"][1]: + return False + + # check bottom + if y+buf < area["vert"][1] - area["height"]: + return False + + # if we reach here we're in the area + 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) + + # right + if area["vert"][0] + area["width"] > bpy.context.region.width: + x = bpy.context.region.width - area["width"] + y = area["vert"][1] + + 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) + + # bottom + if area["vert"][1] - area["height"] < 0: + x = area["vert"][0] + y = area["height"] + + area["vert"] = (x, y) + +def update_area_dimensions(area, w=0, h=0): + area["width"] += w + area["height"] += h + +class QCDMoveWidget(Operator): + """QCD Move Widget""" + bl_idname = "view3d.qcd_move_widget" + bl_label = "QCD Move Widget" + + slots = { + "ONE":1, + "TWO":2, + "THREE":3, + "FOUR":4, + "FIVE":5, + "SIX":6, + "SEVEN":7, + "EIGHT":8, + "NINE":9, + "ZERO":10, + } + + last_type = '' + moved = False + + def modal(self, context, event): + if event.type == 'TIMER': + if self.hover_time and self.hover_time + 0.5 < time.time(): + self.draw_tooltip = True + + context.area.tag_redraw() + return {'RUNNING_MODAL'} + + + context.area.tag_redraw() + + if len(self.areas) == 1: + return {'RUNNING_MODAL'} + + if self.last_type == 'LEFTMOUSE' and event.value == 'PRESS' and event.type == 'MOUSEMOVE': + if mouse_in_area(self.mouse_pos, self.areas["Grab Bar"]): + x_offset = self.areas["Main Window"]["vert"][0] - self.mouse_pos[0] + x = event.mouse_region_x + x_offset + + y_offset = self.areas["Main Window"]["vert"][1] - self.mouse_pos[1] + y = event.mouse_region_y + y_offset + + self.areas["Main Window"]["vert"] = (x, y) + + self.mouse_pos = (event.mouse_region_x, event.mouse_region_y) + + elif event.type == 'MOUSEMOVE': + self.draw_tooltip = False + self.hover_time = None + 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.moved: + bpy.ops.ed.undo_push() + + return {'FINISHED'} + + elif event.value == 'PRESS' and event.type == 'LEFTMOUSE': + if not mouse_in_area(self.mouse_pos, self.areas["Main Window"], 10 * scale_factor()): + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + + if self.moved: + bpy.ops.ed.undo_push() + + return {'FINISHED'} + + for num in range(20): + if not self.areas.get(f"Button {num + 1}", None): + break + + if mouse_in_area(self.mouse_pos, self.areas[f"Button {num + 1}"]): + bpy.ops.view3d.move_to_qcd_slot(slot=str(num + 1), toggle=event.shift) + self.moved = True + + elif event.type in {'RIGHTMOUSE', 'ESC'}: + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + + return {'CANCELLED'} + + if event.value == 'PRESS' and event.type in self.slots: + move_to = self.slots[event.type] + + if event.alt: + move_to += 10 + + if event.shift: + bpy.ops.view3d.move_to_qcd_slot(slot=str(move_to), toggle=True) + else: + bpy.ops.view3d.move_to_qcd_slot(slot=str(move_to), toggle=False) + + self.moved = True + + if event.type != 'MOUSEMOVE' and event.type != 'INBETWEEN_MOUSEMOVE': + self.last_type = event.type + + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + if context.area.type == 'VIEW_3D': + # the arguments we pass the the callback + args = (self, context) + # Add the region OpenGL drawing callback + # draw in view space with 'POST_VIEW' and 'PRE_VIEW' + self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') + self._timer = context.window_manager.event_timer_add(0.1, window=context.window) + + self.mouse_pos = (event.mouse_region_x, event.mouse_region_y) + + self.draw_tooltip = False + + self.hover_time = None + + self.areas = {} + + # MAIN WINDOW BACKGROUND + x = self.mouse_pos[0] - spacer()*2 + y = self.mouse_pos[1] + spacer()*2 + main_window = { + # Top Left Vertex + "vert": (x,y), + "width": 0, + "height": 0, + "value": None + } + account_for_view_bounds(main_window) + + # add main window background to areas + self.areas["Main Window"] = main_window + + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "View3D not found, cannot run operator") + return {'CANCELLED'} + + +def allocate_main_ui(self, context): + main_window = self.areas["Main Window"] + self.areas.clear() + main_window["width"] = 0 + main_window["height"] = 0 + self.areas["Main Window"] = main_window + + cur_width_pos = main_window["vert"][0] + cur_height_pos = main_window["vert"][1] + + # GRAB BAR + grab_bar = { + "vert": main_window["vert"], + "width": 0, + "height": round(23 * scale_factor()), + "value": None + } + + # add grab bar to areas + self.areas["Grab Bar"] = grab_bar + + + # WINDOW TITLE + wt_indent_x = spacer()*2 + wt_y_offset = round(spacer()/2) + window_title = { + "vert": main_window["vert"], + "width": 0, + "height": round(13 * scale_factor()), + "value": "Move Objects to QCD Slots" + } + + x = main_window["vert"][0] + wt_indent_x + y = main_window["vert"][1] - window_title["height"] - wt_y_offset + window_title["vert"] = (x, y) + + # add window title to areas + self.areas["Window Title"] = window_title + + cur_height_pos = window_title["vert"][1] + + + # MAIN BUTTON AREA + button_size = round(20 * scale_factor()) + button_gap = round(1 * scale_factor()) + button_group = 5 + button_group_gap = round(20 * scale_factor()) + button_group_width = button_size * button_group + button_gap * (button_group - 1) + + mba_indent_x = spacer()*2 + mba_outdent_x = spacer()*2 + mba_indent_y = spacer() + x = cur_width_pos + mba_indent_x + y = cur_height_pos - mba_indent_y + main_button_area = { + "vert": (x, y), + "width": 0, + "height": 0, + "value": None + } + + # add main button area to areas + self.areas["Main Button Area"] = main_button_area + + # update current position + cur_width_pos = main_button_area["vert"][0] + cur_height_pos = main_button_area["vert"][1] + + + # BUTTON ROW 1 A + button_row_1_a = { + "vert": main_button_area["vert"], + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 1 A to areas + self.areas["Button Row 1 A"] = button_row_1_a + + # advance width pos to start of next row + cur_width_pos += button_row_1_a["width"] + cur_width_pos += button_group_gap + + # BUTTON ROW 1 B + x = cur_width_pos + y = cur_height_pos + button_row_1_b = { + "vert": (x, y), + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 1 B to areas + self.areas["Button Row 1 B"] = button_row_1_b + + # reset width pos to start of main button area + cur_width_pos = main_button_area["vert"][0] + # update height pos + cur_height_pos -= button_row_1_a["height"] + # add gap between button rows + cur_height_pos -= button_gap + + + # BUTTON ROW 2 A + x = cur_width_pos + y = cur_height_pos + button_row_2_a = { + "vert": (x, y), + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 2 A to areas + self.areas["Button Row 2 A"] = button_row_2_a + + # advance width pos to start of next row + cur_width_pos += button_row_2_a["width"] + cur_width_pos += button_group_gap + + # BUTTON ROW 2 B + x = cur_width_pos + y = cur_height_pos + button_row_2_b = { + "vert": (x, y), + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 2 B to areas + self.areas["Button Row 2 B"] = button_row_2_b + + + # BUTTONS + def get_buttons(button_row, row_num): + cur_width_pos = button_row["vert"][0] + cur_height_pos = button_row["vert"][1] + for num in range(button_group): + slot_num = row_num + num + + qcd_slot = qcd_slots.get_name(f"{slot_num}") + + if qcd_slot: + qcd_laycol = layer_collections[qcd_slot]["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 + y = cur_height_pos + button = { + "vert": (x, y), + "width": button_size, + "height": button_size, + "value": slot_num + } + + self.areas[f"Button {slot_num}"] = button + + # ACTIVE OBJECT ICON + if active_object and active_object in selected_objects and active_object.name in collection_objects: + x = cur_width_pos + round(button_size / 4) + y = cur_height_pos - round(button_size / 4) + active_object_indicator = { + "vert": (x, y), + "width": floor(button_size / 2), + "height": floor(button_size / 2), + "value": None + } + + self.areas[f"Button {slot_num} Active Object Indicator"] = active_object_indicator + + elif not set(selected_objects).isdisjoint(collection_objects): + x = cur_width_pos + round(button_size / 4) + floor(1 * scale_factor()) + y = cur_height_pos - round(button_size / 4) - floor(1 * scale_factor()) + selected_object_indicator = { + "vert": (x, y), + "width": floor(button_size / 2) - floor(1 * scale_factor()), + "height": floor(button_size / 2) - floor(1 * scale_factor()), + "value": None + } + + self.areas[f"Button {slot_num} Selected Object Indicator"] = selected_object_indicator + + elif collection_objects: + x = cur_width_pos + floor(button_size / 4) + y = cur_height_pos - button_size / 2 + 1 * scale_factor() + object_indicator = { + "vert": (x, y), + "width": round(button_size / 2), + "height": round(2 * scale_factor()), + "value": None + } + self.areas[f"Button {slot_num} Object Indicator"] = object_indicator + + else: + x = cur_width_pos + 2 * scale_factor() + y = cur_height_pos - 2 * scale_factor() + X_icon = { + "vert": (x, y), + "width": button_size - 4 * scale_factor(), + "height": button_size - 4 * scale_factor(), + "value": None + } + + self.areas[f"X_icon {slot_num}"] = X_icon + + cur_width_pos += button_size + cur_width_pos += button_gap + + get_buttons(button_row_1_a, 1) + get_buttons(button_row_1_b, 6) + get_buttons(button_row_2_a, 11) + get_buttons(button_row_2_b, 16) + + + # UPDATE DYNAMIC DIMENSIONS + width = button_row_1_a["width"] + button_group_gap + button_row_1_b["width"] + height = button_row_1_a["height"] + button_gap + button_row_2_a["height"] + update_area_dimensions(main_button_area, width, height) + + width = main_button_area["width"] + mba_indent_x + mba_outdent_x + height = main_button_area["height"] + mba_indent_y * 2 + window_title["height"] + wt_y_offset + update_area_dimensions(main_window, width, height) + + update_area_dimensions(grab_bar, main_window["width"]) + + +def draw_callback_px(self, context): + allocate_main_ui(self, context) + + shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') + shader.bind() + + addon_prefs = context.preferences.addons[__package__].preferences + + # main window background + main_window = self.areas["Main Window"] + outline_color = addon_prefs.qcd_ogl_widget_menu_back_outline + background_color = addon_prefs.qcd_ogl_widget_menu_back_inner + draw_rounded_rect(main_window, shader, outline_color[:] + (1,), outline=True) + draw_rounded_rect(main_window, shader, background_color) + + # draw window title + window_title = self.areas["Window Title"] + x = window_title["vert"][0] + y = window_title["vert"][1] + h = window_title["height"] + text = window_title["value"] + text_color = addon_prefs.qcd_ogl_widget_menu_back_text + font_id = 0 + blf.position(font_id, x, y, 0) + blf.size(font_id, int(h), 72) + blf.color(font_id, text_color[0], text_color[1], text_color[2], 1) + blf.draw(font_id, text) + + # refresh shader - not sure why this is needed + shader.bind() + + in_tooltip_area = False + + for num in range(20): + slot_num = num + 1 + qcd_slot = qcd_slots.get_name(f"{slot_num}") + if qcd_slot: + qcd_laycol = layer_collections[qcd_slot]["ptr"] + collection_objects = qcd_laycol.collection.objects + selected_objects = qcd_operators.get_move_selection() + active_object = qcd_operators.get_move_active() + button_area = self.areas[f"Button {slot_num}"] + + # colors + button_color = addon_prefs.qcd_ogl_widget_tool_inner + icon_color = addon_prefs.qcd_ogl_widget_tool_text + if not qcd_laycol.exclude: + button_color = addon_prefs.qcd_ogl_widget_tool_inner_sel + icon_color = addon_prefs.qcd_ogl_widget_tool_text_sel + + if mouse_in_area(self.mouse_pos, button_area): + in_tooltip_area = True + + mod = 0.1 + + if button_color[0] + mod > 1 or button_color[1] + mod > 1 or button_color[2] + mod > 1: + mod = -mod + + button_color = ( + button_color[0] + mod, + button_color[1] + mod, + button_color[2] + mod, + button_color[3] + ) + + + # button roundness + tl = tr = bl = br = 0 + rounding = 5 + + if num < 10: + if not f"{num+2}" in qcd_slots: + tr = rounding + + if not f"{num}" in qcd_slots: + tl = rounding + else: + if not f"{num+2}" in qcd_slots: + br = rounding + + if not f"{num}" in qcd_slots: + bl = rounding + + if num in [0,5]: + tl = rounding + elif num in [4,9]: + tr = rounding + elif num in [10,15]: + bl = rounding + elif num in [14,19]: + br = rounding + + # draw button + outline_color = addon_prefs.qcd_ogl_widget_tool_outline + draw_rounded_rect(button_area, shader, outline_color[:] + (1,), tl, tr, bl, br, outline=True) + draw_rounded_rect(button_area, shader, button_color, tl, tr, bl, br) + + # ACTIVE OBJECT + if active_object and active_object in selected_objects and active_object.name in collection_objects: + active_object_indicator = self.areas[f"Button {slot_num} Active Object Indicator"] + + vertices = get_circle_coords(active_object_indicator) + shader.uniform_float("color", icon_color[:] + (1,)) + batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices}) + + bgl.glEnable(bgl.GL_BLEND) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_BLEND) + + # SELECTED OBJECTS + elif not set(selected_objects).isdisjoint(collection_objects): + selected_object_indicator = self.areas[f"Button {slot_num} Selected Object Indicator"] + + alpha = addon_prefs.qcd_ogl_selected_icon_alpha + vertices = get_circle_coords(selected_object_indicator) + shader.uniform_float("color", icon_color[:] + (alpha,)) + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + + bgl.glLineWidth(2 * scale_factor()) + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_LINE_SMOOTH) + bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_LINE_SMOOTH) + bgl.glDisable(bgl.GL_BLEND) + + # OBJECTS + elif collection_objects: + object_indicator = self.areas[f"Button {slot_num} Object Indicator"] + + alpha = addon_prefs.qcd_ogl_objects_icon_alpha + vertices, indices = get_coords(object_indicator) + shader.uniform_float("color", icon_color[:] + (alpha,)) + batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) + + bgl.glEnable(bgl.GL_BLEND) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_BLEND) + + + # X ICON + else: + X_icon = self.areas[f"X_icon {slot_num}"] + X_icon_color = addon_prefs.qcd_ogl_widget_menu_back_text + + vertices, indices = get_x_coords(X_icon) + shader.uniform_float("color", X_icon_color[:] + (1,)) + batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) + + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_POLYGON_SMOOTH) + bgl.glHint(bgl.GL_POLYGON_SMOOTH_HINT, bgl.GL_NICEST) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_POLYGON_SMOOTH) + bgl.glDisable(bgl.GL_BLEND) + + if in_tooltip_area: + if self.draw_tooltip: + draw_tooltip(self, context, shader,"Move Object To QCD Slot\n * Shift-Click to toggle objects\' slot") + self.hover_time = None + + else: + if not self.hover_time: + self.hover_time = time.time() + + +def draw_tooltip(self, context, shader, message): + addon_prefs = context.preferences.addons[__package__].preferences + + font_id = 0 + line_height = 11 * scale_factor() + text_color = addon_prefs.qcd_ogl_widget_tooltip_text + blf.size(font_id, int(line_height), 72) + blf.color(font_id, text_color[0], text_color[1], text_color[2], 1) + + lines = message.split("\n") + longest = [0,""] + num_lines = len(lines) + + for line in lines: + if len(line) > longest[0]: + longest[0] = len(line) + longest[1] = line + + w, h = blf.dimensions(font_id, longest[1]) + + line_spacer = 1 * scale_factor() + padding = 4 * scale_factor() + + # draw background + tooltip = { + "vert": self.mouse_pos, + "width": w + spacer()*2, + "height": (line_height * num_lines + line_spacer * num_lines) + padding*3, + "value": None + } + + x = tooltip["vert"][0] - spacer()*2 + y = tooltip["vert"][1] + tooltip["height"] + round(5 * scale_factor()) + tooltip["vert"] = (x, y) + + account_for_view_bounds(tooltip) + + outline_color = addon_prefs.qcd_ogl_widget_tooltip_outline + background_color = addon_prefs.qcd_ogl_widget_tooltip_inner + draw_rounded_rect(tooltip, shader, outline_color[:] + (1,), outline=True) + draw_rounded_rect(tooltip, shader, background_color) + + line_pos = padding + line_height + # draw text + for num, line in enumerate(lines): + x = tooltip["vert"][0] + spacer() + y = tooltip["vert"][1] - line_pos + blf.position(font_id, x, y, 0) + blf.draw(font_id, line) + + line_pos += line_height + line_spacer diff --git a/object_collection_manager/qcd_operators.py b/object_collection_manager/qcd_operators.py new file mode 100644 index 00000000..db58dc4b --- /dev/null +++ b/object_collection_manager/qcd_operators.py @@ -0,0 +1,286 @@ +# ##### 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 ##### + +# Copyright 2011, Ryan Inch + +import bpy + +from bpy.types import ( + Operator, +) + +from bpy.props import ( + BoolProperty, + StringProperty, + IntProperty +) + +from .internals import ( + layer_collections, + qcd_slots, + update_property_group, + get_modifiers, +) + +from .operators import rto_history + +move_triggered = False +move_selection = [] +move_active = None + +def get_move_selection(): + global move_selection + + if not move_selection: + move_selection = bpy.context.selected_objects + + return move_selection + +def get_move_active(): + global move_active + global move_selection + + if not move_active: + move_active = bpy.context.view_layer.objects.active + + if move_active not in get_move_selection(): + move_active = None + + try: + move_active.name + + except: + move_active = None + move_selection = [] + + # update header widget + cm = bpy.context.scene.collection_manager + cm.update_header.clear() + new_update_header = cm.update_header.add() + new_update_header.name = "updated" + + return move_active + +class MoveToQCDSlot(Operator): + '''Move object(s) to QCD slot''' + bl_label = "Move To QCD Slot" + bl_idname = "view3d.move_to_qcd_slot" + bl_options = {'REGISTER', 'UNDO'} + + slot: StringProperty() + toggle: BoolProperty() + + def execute(self, context): + global qcd_slots + global layer_collections + global move_triggered + + selected_objects = get_move_selection() + active_object = get_move_active() + move_triggered = True + qcd_laycol = qcd_slots.get_name(self.slot) + + if qcd_laycol: + qcd_laycol = layer_collections[qcd_laycol]["ptr"] + + else: + return {'CANCELLED'} + + + if not selected_objects: + return {'CANCELLED'} + + # adds object to slot + if self.toggle: + if not active_object: + active_object = selected_objects[0] + + if not active_object.name in qcd_laycol.collection.objects: + for obj in selected_objects: + if obj.name not in qcd_laycol.collection.objects: + qcd_laycol.collection.objects.link(obj) + + else: + for obj in selected_objects: + if obj.name in qcd_laycol.collection.objects: + + if len(obj.users_collection) == 1: + continue + + qcd_laycol.collection.objects.unlink(obj) + + + # moves object to slot + else: + for obj in selected_objects: + if obj.name not in qcd_laycol.collection.objects: + qcd_laycol.collection.objects.link(obj) + + for collection in obj.users_collection: + qcd_idx = qcd_slots.get_idx(collection.name) + if qcd_idx != self.slot: + collection.objects.unlink(obj) + + + if not context.active_object: + try: + context.view_layer.objects.active = active_object + except: + pass + + # update header UI + cm = bpy.context.scene.collection_manager + cm.update_header.clear() + new_update_header = cm.update_header.add() + new_update_header.name = "updated" + + return {'FINISHED'} + + +class ViewMoveQCDSlot(Operator): + ''' * Shift-Click to toggle QCD slots\n * Ctrl-Click to move objects to QCD slot\n * Ctrl-Shift-Click to toggle objects\' slot''' + bl_label = "View QCD Slot" + bl_idname = "view3d.view_move_qcd_slot" + bl_options = {'REGISTER', 'UNDO'} + + slot: StringProperty() + + def invoke(self, context, event): + global layer_collections + global qcd_history + + modifiers = get_modifiers(event) + + 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'} + + else: + bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=False) + return {'FINISHED'} + +class ViewQCDSlot(Operator): + '''View objects in QCD slot''' + bl_label = "View QCD Slot" + bl_idname = "view3d.view_qcd_slot" + bl_options = {'REGISTER', 'UNDO'} + + slot: StringProperty() + toggle: BoolProperty() + + def execute(self, context): + global qcd_slots + global layer_collections + global rto_history + + qcd_laycol = qcd_slots.get_name(self.slot) + + if qcd_laycol: + qcd_laycol = layer_collections[qcd_laycol]["ptr"] + + else: + return {'CANCELLED'} + + if self.toggle: + # get current child exclusion state + child_exclusion = [] + + laycol_iter_list = [qcd_laycol.children] + while len(laycol_iter_list) > 0: + new_laycol_iter_list = [] + for laycol_iter in laycol_iter_list: + for layer_collection in laycol_iter: + child_exclusion.append([layer_collection, layer_collection.exclude]) + if len(layer_collection.children) > 0: + new_laycol_iter_list.append(layer_collection.children) + + laycol_iter_list = new_laycol_iter_list + + # toggle exclusion of qcd_laycol + qcd_laycol.exclude = not qcd_laycol.exclude + + # set correct state for all children + for laycol in child_exclusion: + laycol[0].exclude = laycol[1] + + # set layer as active layer collection + context.view_layer.active_layer_collection = qcd_laycol + + else: + for laycol in layer_collections.values(): + if laycol["name"] != qcd_laycol.name: + laycol["ptr"].exclude = True + + qcd_laycol.exclude = False + + # exclude all children + laycol_iter_list = [qcd_laycol.children] + while len(laycol_iter_list) > 0: + new_laycol_iter_list = [] + for laycol_iter in laycol_iter_list: + for layer_collection in laycol_iter: + layer_collection.exclude = True + if len(layer_collection.children) > 0: + new_laycol_iter_list.append(layer_collection.children) + + laycol_iter_list = new_laycol_iter_list + + # set layer as active layer collection + context.view_layer.active_layer_collection = qcd_laycol + + # update header UI + cm = bpy.context.scene.collection_manager + cm.update_header.clear() + new_update_header = cm.update_header.add() + new_update_header.name = "updated" + + view_layer = context.view_layer.name + if view_layer in rto_history["exclude"]: + del rto_history["exclude"][view_layer] + if view_layer in rto_history["exclude_all"]: + del rto_history["exclude_all"][view_layer] + + return {'FINISHED'} + + +class RenumerateQCDSlots(Operator): + '''Re-numerate QCD slots\n * Ctrl-Click to include collections marked by the user as non QCD slots''' + bl_label = "Re-numerate QCD Slots" + bl_idname = "view3d.renumerate_qcd_slots" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + global qcd_slots + + qcd_slots.clear() + + if event.ctrl: + qcd_slots.overrides.clear() + + update_property_group(context, renumerate=True) + + return {'FINISHED'} diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 88e9d0cc..b29f5c59 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -31,6 +31,7 @@ from .internals import ( expanded, get_max_lvl, layer_collections, + qcd_slots, update_collection_tree, update_property_group, ) @@ -41,6 +42,13 @@ from .operators import ( phantom_history, ) +from . import qcd_operators + + +preview_collections = {} +last_icon_theme_text = None +last_icon_theme_text_sel = None + class CollectionManager(Operator): bl_label = "Collection Manager" @@ -97,6 +105,10 @@ class CollectionManager(Operator): sec1.operator("view3d.expand_all_items", text=text) + renum = toggle_row.row() + renum.alignment = 'LEFT' + renum.operator("view3d.renumerate_qcd_slots") + for laycol in collection_tree: if laycol["has_children"]: sec1.enabled = True @@ -201,54 +213,6 @@ class CollectionManager(Operator): return wm.invoke_popup(self, width=width) -def update_selection(self, context): - cm = context.scene.collection_manager - - if cm.cm_list_index == -1: - return - - selected_item = cm.cm_list_collection[cm.cm_list_index] - layer_collection = layer_collections[selected_item.name]["ptr"] - - context.view_layer.active_layer_collection = layer_collection - - -def filter_items_by_name_insensitive(pattern, bitflag, items, propname="name", flags=None, reverse=False): - """ - Set FILTER_ITEM for items which name matches filter_name one (case-insensitive). - pattern is the filtering pattern. - propname is the name of the string property to use for filtering. - flags must be a list of integers the same length as items, or None! - return a list of flags (based on given flags if not None), - or an empty list if no flags were given and no filtering has been done. - """ - import fnmatch - - if not pattern or not items: # Empty pattern or list = no filtering! - return flags or [] - - if flags is None: - flags = [0] * len(items) - - # Make pattern case-insensitive - pattern = pattern.lower() - - # Implicitly add heading/trailing wildcards. - pattern = "*" + pattern + "*" - - for i, item in enumerate(items): - name = getattr(item, propname, None) - - # Make name case-insensitive - name = name.lower() - - # This is similar to a logical xor - if bool(name and fnmatch.fnmatch(name, pattern)) is not bool(reverse): - flags[i] |= bitflag - - return flags - - class CM_UL_items(UIList): last_filter_value = "" @@ -257,6 +221,11 @@ class CM_UL_items(UIList): default=False, description="Filter collections by selected items" ) + filter_by_qcd: BoolProperty( + name="Filter By QCD", + default=False, + description="Filter collections to only show QCD slots" + ) def draw_item(self, context, layout, data, item, icon, active_data,active_propname, index): self.use_filter_show = True @@ -299,6 +268,10 @@ class CM_UL_items(UIList): row.label(icon='GROUP') + QCD = row.row() + QCD.scale_x = 0.4 + QCD.prop(item, "qcd_slot", text="") + name_row = row.row() #if rename[0] and index == cm.cm_list_index: @@ -410,6 +383,7 @@ class CM_UL_items(UIList): subrow = row.row(align=True) subrow.prop(self, "filter_by_selected", text="", icon='SNAP_VOLUME') + subrow.prop(self, "filter_by_qcd", text="", icon='EVENT_Q') def filter_items(self, context, data, propname): flt_flags = [] @@ -430,6 +404,13 @@ class CM_UL_items(UIList): if not set(context.selected_objects).isdisjoint(collection.objects): flt_flags[idx] |= self.bitflag_filter_item + elif self.filter_by_qcd: + flt_flags = [0] * len(list_items) + + for idx, item in enumerate(list_items): + if item.qcd_slot: + flt_flags[idx] |= self.bitflag_filter_item + else: # display as treeview flt_flags = [self.bitflag_filter_item] * len(list_items) @@ -462,3 +443,172 @@ class CMRestrictionTogglesPanel(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) + + +def view3d_header_qcd_slots(self, context): + layout = self.layout + + idx = 1 + + split = layout.split() + col = split.column(align=True) + row = col.row(align=True) + row.scale_y = 0.5 + + update_collection_tree(context) + + for x in range(20): + qcd_slot = qcd_slots.get_name(str(x+1)) + + if qcd_slot: + qcd_laycol = layer_collections[qcd_slot]["ptr"] + collection_objects = qcd_laycol.collection.objects + selected_objects = qcd_operators.get_move_selection() + active_object = qcd_operators.get_move_active() + + icon_value = 0 + + # if the active object is in the current collection use a custom icon + if (active_object and active_object in selected_objects and + 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): + icon = 'LAYER_USED' + + # If there are objects use LAYER_USED + elif collection_objects: + icon = 'NONE' + active_icon = get_active_icon(context, qcd_laycol) + icon_value = active_icon.icon_id + + else: + icon = 'BLANK1' + + + prop = row.operator("view3d.view_move_qcd_slot", text="", icon=icon, + icon_value=icon_value, depress=not qcd_laycol.exclude) + prop.slot = str(x+1) + + else: + row.label(text="", icon='X') + + + if idx%5==0: + row.separator() + + if idx == 10: + row = col.row(align=True) + row.scale_y = 0.5 + + idx += 1 + + +def get_active_icon(context, qcd_laycol): + global last_icon_theme_text + global last_icon_theme_text_sel + + tool_theme = context.preferences.themes[0].user_interface.wcol_tool + pcoll = preview_collections["icons"] + + if qcd_laycol.exclude: + theme_color = tool_theme.text + last_theme_color = last_icon_theme_text + icon = pcoll["active_icon_text"] + + else: + theme_color = tool_theme.text_sel + last_theme_color = last_icon_theme_text_sel + icon = pcoll["active_icon_text_sel"] + + if last_theme_color == None or theme_color.hsv != last_theme_color: + update_icon(pcoll["active_icon_base"], icon, theme_color) + + if qcd_laycol.exclude: + last_icon_theme_text = theme_color.hsv + + else: + last_icon_theme_text_sel = theme_color.hsv + + return icon + + +def update_icon(base, icon, theme_color): + icon.icon_pixels = base.icon_pixels + colored_icon = [] + + for offset in range(len(icon.icon_pixels)): + idx = offset * 4 + + r = icon.icon_pixels_float[idx] + g = icon.icon_pixels_float[idx+1] + b = icon.icon_pixels_float[idx+2] + a = icon.icon_pixels_float[idx+3] + + # add back some brightness and opacity blender takes away from the custom icon + r = min(r+r*0.2,1) + g = min(g+g*0.2,1) + b = min(b+b*0.2,1) + a = min(a+a*0.2,1) + + # make the icon follow the theme color (assuming the icon is white) + r *= theme_color.r + g *= theme_color.g + b *= theme_color.b + + colored_icon.append(r) + colored_icon.append(g) + colored_icon.append(b) + colored_icon.append(a) + + icon.icon_pixels_float = colored_icon + + +def update_selection(self, context): + cm = context.scene.collection_manager + + if cm.cm_list_index == -1: + return + + selected_item = cm.cm_list_collection[cm.cm_list_index] + layer_collection = layer_collections[selected_item.name]["ptr"] + + context.view_layer.active_layer_collection = layer_collection + + +def filter_items_by_name_insensitive(pattern, bitflag, items, propname="name", flags=None, reverse=False): + """ + Set FILTER_ITEM for items which name matches filter_name one (case-insensitive). + pattern is the filtering pattern. + propname is the name of the string property to use for filtering. + flags must be a list of integers the same length as items, or None! + return a list of flags (based on given flags if not None), + or an empty list if no flags were given and no filtering has been done. + """ + import fnmatch + + if not pattern or not items: # Empty pattern or list = no filtering! + return flags or [] + + if flags is None: + flags = [0] * len(items) + + # Make pattern case-insensitive + pattern = pattern.lower() + + # Implicitly add heading/trailing wildcards. + pattern = "*" + pattern + "*" + + for i, item in enumerate(items): + name = getattr(item, propname, None) + + # Make name case-insensitive + name = name.lower() + + # This is similar to a logical xor + if bool(name and fnmatch.fnmatch(name, pattern)) is not bool(reverse): + flags[i] |= bitflag + + return flags -- cgit v1.2.3