Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Eisel <eiseljulian@gmail.com>2020-03-16 16:32:23 +0300
committerJulian Eisel <eiseljulian@gmail.com>2020-03-16 16:32:23 +0300
commit0d3ae1fca61d2b56aa5d9ae47b44c48b4ca761c3 (patch)
tree08901ef13491bfdc22946d2592bc3d76b773368a
parent1831c51d5f3d8c2b156a9492f167358ebbc5256f (diff)
parentb752de9e0da4e8ad694de25497275f66168a2df4 (diff)
Merge branch 'master' into soc-2019-openxr
-rw-r--r--add_mesh_geodesic_domes/__init__.py2
-rw-r--r--add_mesh_geodesic_domes/third_domes_panel_271.py16
-rw-r--r--curve_tools/__init__.py394
-rw-r--r--curve_tools/cad.py18
-rw-r--r--curve_tools/curves.py6
-rw-r--r--curve_tools/internal.py40
-rw-r--r--curve_tools/intersections.py6
-rw-r--r--curve_tools/operators.py149
-rw-r--r--curve_tools/toolpath.py125
-rwxr-xr-xio_scene_gltf2/__init__.py22
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_export.py5
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_image.py4
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py62
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_image.py2
-rwxr-xr-xio_scene_gltf2/blender/imp/gltf2_blender_scene.py22
-rw-r--r--io_scene_gltf2/io/exp/gltf2_io_user_extensions.py13
-rw-r--r--object_collection_manager/__init__.py118
-rw-r--r--object_collection_manager/icons/minus.pngbin0 -> 1934 bytes
-rw-r--r--object_collection_manager/internals.py200
-rw-r--r--object_collection_manager/operators.py55
-rw-r--r--object_collection_manager/preferences.py512
-rw-r--r--object_collection_manager/qcd_move_widget.py969
-rw-r--r--object_collection_manager/qcd_operators.py286
-rw-r--r--object_collection_manager/ui.py253
24 files changed, 2947 insertions, 332 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"
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]
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
index 49f6f470..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, 38),
+ "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')
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/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)
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:
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
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))
diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py
index 39a22906..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,9,3),
+ "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
--- /dev/null
+++ b/object_collection_manager/icons/minus.png
Binary files differ
diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py
index 5267b1c6..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
@@ -80,7 +217,41 @@ 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)
+
+ # 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):
@@ -119,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 7f693ac9..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,
@@ -73,8 +74,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 +85,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 +121,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 +157,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
@@ -1376,6 +1419,7 @@ class CMRemoveCollectionOperator(Operator):
def execute(self, context):
global rto_history
+ global qcd_slots
cm = context.scene.collection_manager
@@ -1406,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 dcd804fa..b29f5c59 100644
--- a/object_collection_manager/ui.py
+++ b/object_collection_manager/ui.py
@@ -31,15 +31,24 @@ from .internals import (
expanded,
get_max_lvl,
layer_collections,
+ qcd_slots,
update_collection_tree,
update_property_group,
)
from .operators import (
rto_history,
+ expand_history,
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"
@@ -96,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
@@ -200,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 = ""
@@ -256,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
@@ -277,8 +247,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
@@ -296,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:
@@ -407,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 = []
@@ -427,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)
@@ -459,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