├── .gitignore
├── README.md
├── __init__.py
├── license
├── operators
├── align_between_two.py
├── align_origin_to_normal.py
├── assign_vertex_color.py
├── cursor_origin
│ └── mesh.py
├── cursor_rotate.py
├── curve_spline_type.py
├── curve_subdivide.py
├── drag_snap.py
├── drag_snap_cursor.py
├── drag_snap_uv.py
├── easy_mod_array.py
├── easy_mod_curve.py
├── easy_mod_shwarp.py
├── executor.py
├── grid_from_active.py
├── hotkeys
│ ├── load_hotkeys.py
│ └── save_hotkeys.py
├── image_reload.py
├── iops.py
├── library_reload.py
├── materials_from_textures.py
├── maya_isolate.py
├── mesh_convert_selection.py
├── mesh_copy_edges_angle.py
├── mesh_copy_edges_length.py
├── mesh_cursor_bisect.py
├── mesh_quick_snap.py
├── mesh_to_grid.py
├── mesh_uv_channel_hop.py
├── modes.py
├── mouseover_fill_select.py
├── object_align_to_face.py
├── object_auto_smooth.py
├── object_drop_it.py
├── object_kitbash_grid.py
├── object_match_transform_active.py
├── object_name_from_active.py
├── object_normalize.py
├── object_replace.py
├── object_rotate.py
├── object_three_point_rotation.py
├── object_uvmaps_add_remove.py
├── object_uvmaps_cleaner.py
├── object_visual_origin.py
├── open_asset_in_current_blender.py
├── outliner_collection_ops.py
├── preferences
│ └── io_addon_preferences.py
├── render_asset_thumbnail.py
├── run_text.py
├── save_load_space_data.py
├── snap_combos.py
├── split_screen_area.py
├── split_screen_area_new.py
├── ui_prop_switch.py
└── z_ops.py
├── prefs
├── addon_preferences.py
├── addon_properties.py
├── hotkeys_default.py
├── iops_prefs.py
└── iops_prefs_list.py
├── ui
├── iops_data_panel.py
├── iops_pie_edit.py
├── iops_pie_menu.py
├── iops_pie_split.py
└── iops_tm_panel.py
└── utils
├── draw_stats.py
├── functions.py
├── iops_dict.py
└── split_areas_dict.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # User Hotkeys
2 | prefs/hotkeys_user.py
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 |
8 | # Installer logs
9 | pip-log.txt
10 | pip-delete-this-directory.txt
11 |
12 | # Bookmarks file and recents
13 | config/bookmarks.txt
14 | config/recent-files.txt
15 |
16 | # VSCode
17 | .vscode/*
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # InteractionOps or iOps
4 | It is a set of operators which boost Blender interactivity between user and application by using only five functional buttons.
5 |
6 | ## Documentation:
7 | https://interactionops-docs.readthedocs.io/en/latest/index.html
8 |
9 | ## Ideas and support:
10 | BlenderArtists forum page
11 | https://blenderartists.org/t/interactionops-iops/
12 |
13 | ## Authors:
14 | Cyrill Vitkovskiy:
15 | https://www.artstation.com/furash
16 |
17 | Titus Lavrov:
18 | https://www.artstation.com/tituslvr
19 | Also check my page on gumroad:
20 | https://gumroad.com/titus
21 |
22 | ## Special thanks:
23 | For author of qBlocker
24 | https://gumroad.com/sanislovart
25 |
26 | jayanamgames youtube channel:
27 | https://www.youtube.com/user/jayanamgames
28 |
29 | Many thanks!
30 |
31 |
32 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | Copyright (C) 2017-2020 Tit Lavrov, Cyrill Vitkovskiy, Titus.mailbox@gmail.com
2 |
3 | This program is free software: you can redistribute it and/or modify
4 | it under the terms of the GNU General Public License as published by
5 | the Free Software Foundation, either version 3 of the License, or
6 | (at your option) any later version.
7 |
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 |
13 | You should have received a copy of the GNU General Public License
14 | along with this program. If not, see .
15 |
--------------------------------------------------------------------------------
/operators/align_between_two.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | from bpy.props import (
4 | BoolProperty,
5 | EnumProperty,
6 | IntProperty
7 | )
8 |
9 | from ..utils.functions import get_active_and_selected, ShowMessageBox
10 |
11 |
12 | class IOPS_OT_Align_between_two(bpy.types.Operator):
13 | """Align active object between two selected objects. Works for two objects also."""
14 |
15 | bl_idname = "iops.object_align_between_two"
16 | bl_label = "Align Between Two"
17 | bl_options = {"REGISTER", "UNDO"}
18 |
19 | track_axis: EnumProperty(
20 | name="Track",
21 | description="Track axis",
22 | items=[
23 | ("X", "X", "", "", 0),
24 | ("Y", "Y", "", "", 1),
25 | ("Z", "Z", "", "", 2),
26 | ("-X", "-X", "", "", 3),
27 | ("-Y", "-Y", "", "", 4),
28 | ("-Z", "-Z", "", "", 5),
29 | ],
30 | default="Y",
31 | )
32 | up_axis: EnumProperty(
33 | name="Up",
34 | description="Up axis",
35 | items=[
36 | ("X", "X", "", "", 0),
37 | ("Y", "Y", "", "", 1),
38 | ("Z", "Z", "", "", 2),
39 | ],
40 | default="Z",
41 | )
42 | align: BoolProperty(
43 | name="Align",
44 | description="Align Duplicates Between Selected Objects",
45 | default=False,
46 | )
47 |
48 | count: IntProperty(
49 | name="Count",
50 | description="Number of Duplicates",
51 | default=1,
52 | soft_min=0,
53 | soft_max=100000000,
54 | )
55 |
56 | select_duplicated: BoolProperty(
57 | name="Select Duplicated",
58 | description="Enabled = Select Duplicated Objects, Disabled = Keep Selection",
59 | default=True,
60 | )
61 |
62 | def align_between(self):
63 | sequence = []
64 | active, objects = get_active_and_selected()
65 | if len(bpy.context.selected_objects) == 2:
66 | axis = active.location - objects[-1].location
67 |
68 | A = active.location
69 | B = objects[-1].location
70 |
71 | for ip in range(self.count):
72 | p = 1 / (self.count + 1) * (ip + 1)
73 | point = (1 - p) * A + p * B
74 | sequence.append(point)
75 | elif len(bpy.context.selected_objects) == 3:
76 | posA, posB = [ob.location for ob in objects]
77 | axis = posA - posB
78 |
79 | for idx in range(len(objects) - 1):
80 | A = objects[idx].location
81 | B = objects[idx + 1].location
82 |
83 | for ip in range(self.count):
84 | p = 1 / (self.count + 1) * (ip + 1)
85 | point = (1 - p) * A + p * B
86 | sequence.append(point)
87 | else:
88 | ShowMessageBox("Must be 2 or 3 Objects Selected.")
89 | return
90 |
91 | collection = bpy.data.collections.new("Objects Between")
92 | bpy.context.scene.collection.children.link(collection)
93 | new_objects = []
94 | for p in sequence:
95 | new_ob = active.copy()
96 | new_ob.data = active.data.copy()
97 | # position
98 | new_ob.location = p
99 | # rotation
100 | if self.align:
101 | new_ob.rotation_mode = "QUATERNION"
102 | new_ob.rotation_quaternion = axis.to_track_quat(
103 | self.track_axis, self.up_axis
104 | )
105 | new_ob.rotation_mode = "XYZ"
106 |
107 | collection.objects.link(new_ob)
108 | new_ob.select_set(False)
109 | new_objects.append(new_ob)
110 |
111 | if self.select_duplicated:
112 | active.select_set(False)
113 | for ob in objects:
114 | ob.select_set(False)
115 | for ob in new_objects:
116 | ob.select_set(True)
117 | bpy.context.view_layer.objects.active = new_objects[-1]
118 |
119 | def execute(self, context):
120 | if self.track_axis != self.up_axis:
121 | self.align_between()
122 | self.report({"INFO"}, "Aligned!")
123 | else:
124 | self.report({"WARNING"}, "SAME AXIS")
125 | return {"FINISHED"}
126 |
127 | def draw(self, context):
128 | layout = self.layout
129 | col = layout.column(align=True)
130 | #
131 | col.prop(self, "mode")
132 | col.separator()
133 | col.prop(self, "track_axis")
134 | col.prop(self, "up_axis")
135 | col.separator()
136 | col.prop(self, "align")
137 | col.prop(self, "count")
138 | col.separator()
139 | col.prop(self, "select_duplicated")
140 |
--------------------------------------------------------------------------------
/operators/align_origin_to_normal.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.props import (
3 | FloatVectorProperty
4 | )
5 | import bmesh
6 | from mathutils import Matrix
7 |
8 |
9 | class IOPS_OT_AlignOriginToNormal(bpy.types.Operator):
10 | """Align object to selected face"""
11 |
12 | bl_idname = "iops.mesh_align_origin_to_normal"
13 | bl_label = "MESH: Align origin to face normal"
14 | bl_options = {"REGISTER", "UNDO"}
15 |
16 | loc: FloatVectorProperty()
17 | orig_mx = []
18 |
19 | @classmethod
20 | def poll(self, context):
21 | return (
22 | context.area.type == "VIEW_3D"
23 | and context.mode == "EDIT_MESH"
24 | and len(context.view_layer.objects.selected) != 0
25 | and context.view_layer.objects.active.type == "MESH"
26 | )
27 |
28 | def align_origin_to_normal(self):
29 |
30 | bpy.ops.view3d.snap_cursor_to_selected()
31 | bpy.ops.object.mode_set(mode="OBJECT")
32 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR")
33 | bpy.ops.object.mode_set(mode="EDIT")
34 |
35 | obj = bpy.context.view_layer.objects.active
36 | mx = obj.matrix_world.copy()
37 | loc = mx.to_translation() # Store location
38 | scale = mx.to_scale() # Store scale
39 | polymesh = obj.data
40 | bm = bmesh.from_edit_mesh(polymesh)
41 | face = bm.faces.active
42 |
43 | # Return face tangent based on longest edge.
44 | tangent = face.calc_tangent_edge()
45 |
46 | # Build vectors for new matrix
47 | n = face.normal
48 | t = tangent
49 | c = n.cross(t)
50 |
51 | # Assemble new matrix
52 | mx_new = Matrix((t * -1.0, c * -1.0, n)).transposed().to_4x4()
53 |
54 | # New matrix rotation part
55 | new_rot = mx_new.to_euler()
56 |
57 | # Apply new matrix
58 | obj.matrix_world = mx_new.inverted()
59 | obj.location = loc
60 | obj.scale = scale
61 |
62 | bpy.ops.object.mode_set(mode="OBJECT")
63 | bpy.ops.object.transform_apply(
64 | location=False, rotation=True, scale=False, properties=False
65 | )
66 | obj.rotation_euler = new_rot
67 |
68 | def execute(self, context):
69 | if context.object and context.area.type == "VIEW_3D":
70 | # Apply transform so it works multiple times
71 | bpy.ops.object.mode_set(mode="OBJECT")
72 | bpy.ops.object.transform_apply(
73 | location=False, rotation=True, scale=False, properties=False
74 | )
75 | bpy.ops.object.mode_set(mode="EDIT")
76 |
77 | # Initialize axis and assign starting values for object's location
78 | self.axis_rotate = "Z"
79 | self.flip = True
80 | self.edge_idx = 0
81 | self.counter = 0
82 | self.align_origin_to_normal()
83 |
84 | return {"FINISHED"}
85 |
86 | else:
87 | self.report({"WARNING"}, "No active object, could not finish")
88 | return {"FINISHED"}
89 |
--------------------------------------------------------------------------------
/operators/assign_vertex_color.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import bmesh
3 | from bpy.props import FloatProperty, BoolProperty
4 |
5 | class IOPS_OT_VertexColorAssign(bpy.types.Operator):
6 | """Assign Vertex color in editr mode to selected vertecies"""
7 |
8 | bl_idname = "iops.mesh_assign_vertex_color"
9 | bl_label = "Assign Vertex color in editr mode to selected vertecies"
10 | bl_options = {"REGISTER", "UNDO"}
11 |
12 | use_active_color: BoolProperty(
13 | name="Use Active Color Attribute",
14 | default=True
15 | )
16 |
17 | col_attr_name: bpy.props.StringProperty(
18 | name="Color Attribute Name",
19 | default="Color"
20 | )
21 | fill_color_black: BoolProperty(
22 | name="Fill Black",
23 | #description="Fill selected vertecies with black color",
24 | default=False
25 | )
26 | fill_color_white: BoolProperty(
27 | name="Fill White",
28 | #descritpion="Fill selected vertecies with white color",
29 | default=False
30 | )
31 | fill_color_grey: BoolProperty(
32 | name="Fill Grey",
33 | #description="Fill selected vertecies with grey color",
34 | default=False
35 | )
36 |
37 | domain: bpy.props.EnumProperty(
38 | name="Domain",
39 | description="Domain of the color attribute",
40 | items=[("POINT", "Point", "Point"), ("CORNER", "Corner", "Corner")],
41 | default="POINT"
42 | )
43 |
44 | attr_type: bpy.props.EnumProperty(
45 | name="Attribute Type",
46 | description="Type of the color attribute",
47 | items=[("FLOAT_COLOR", "Float Color", "Float Color"), ("BYTE_COLOR", "Byte Color", "Byte Color")],
48 | default="FLOAT_COLOR"
49 | )
50 |
51 | @classmethod
52 | def poll(cls, context):
53 | return context.object and context.object.type == "MESH"
54 |
55 | def execute(self, context):
56 | color_black = (0.0, 0.0, 0.0, 1.0)
57 | color_white = (1.0, 1.0, 1.0, 1.0)
58 | color_picker = context.scene.IOPS.iops_vertex_color
59 |
60 | color = color_picker
61 |
62 | if self.fill_color_black:
63 | self.fill_color_black = False
64 | color = color_black
65 |
66 | if self.fill_color_grey:
67 | self.fill_color_grey = False
68 | color = (0.5, 0.5, 0.5, 1.0)
69 |
70 | if self.fill_color_white:
71 | self.fill_color_white = False
72 | color = color_white
73 |
74 | sel = [obj for obj in context.selected_objects]
75 | # Create color attribute if not exists
76 | if not self.use_active_color:
77 | for obj in sel:
78 | if self.col_attr_name not in obj.data.color_attributes:
79 | obj.data.color_attributes.new(
80 | self.col_attr_name, self.attr_type, self.domain
81 | )
82 | # Set color attribute as active
83 | attr_name = obj.data.color_attributes[self.col_attr_name]
84 | obj.data.color_attributes.active_color = attr_name
85 | else:
86 | try:
87 | attr_name = context.object.data.color_attributes.active_color.name
88 | except AttributeError:
89 | # Create new color attribute if none exists
90 | context.object.data.color_attributes.new(
91 | self.col_attr_name, self.attr_type, self.domain
92 | )
93 | attr_name = self.col_attr_name
94 | context.object.data.color_attributes.active_color = context.object.data.color_attributes[attr_name]
95 |
96 | attr_type = context.object.data.color_attributes.active_color.data_type
97 |
98 | if sel and context.mode == "EDIT_MESH":
99 | # IF EDIT MODE
100 | attr_domain = context.object.data.color_attributes.active_color.domain # POINT or CORNER
101 | col_layer = None
102 | if attr_domain == "POINT":
103 | for obj in sel:
104 | bm = bmesh.new()
105 | bm = bmesh.from_edit_mesh(obj.data)
106 | if attr_type == "FLOAT_COLOR":
107 | col_layer = bm.verts.layers.float_color[attr_name]
108 | elif attr_type == "BYTE_COLOR":
109 | col_layer = bm.verts.layers.color[attr_name]
110 | verts = [v for v in bm.verts if v.select]
111 | if col_layer:
112 | for vert in verts:
113 | vert[col_layer] = color
114 | elif attr_domain == "CORNER":
115 | for obj in sel:
116 | bm = bmesh.new()
117 | bm = bmesh.from_edit_mesh(obj.data)
118 | if attr_type == "FLOAT_COLOR":
119 | col_layer = bm.loops.layers.float_color[attr_name]
120 | elif attr_type == "BYTE_COLOR":
121 | col_layer = bm.loops.layers.color[attr_name]
122 | if col_layer:
123 | for f in bm.faces:
124 | if any(v.select for v in f.verts):
125 | for loop in f.loops:
126 | loop[col_layer] = color
127 |
128 | bmesh.update_edit_mesh(context.object.data)
129 | bm.free()
130 |
131 | elif context.mode == "OBJECT":
132 | self.report(
133 | {"WARNING"}, "OBJECT MODE. Will be implemented soon. Crashing for now."
134 | )
135 | # IF OBJECT MODE
136 | # # BOTH ARE CRASHING ON POST OPERATOR COLOR CHANGE
137 | # for obj in sel:
138 | # # NON-Bmesh way
139 | # for poly in obj.data.polygons:
140 | # for i in poly.loop_indices:
141 | # obj.data.color_attributes[self.col_attr_name].data[i].color = color
142 |
143 | # # Bmesh way
144 | # bm = bmesh.new()
145 | # bm.from_mesh(obj.data)
146 | # verts = [v for v in bm.verts]
147 | # col_layer = bm.verts.layers.float_color[self.col_attr_name]
148 | # for v in verts:
149 | # v[col_layer] = color
150 |
151 | # bm.to_mesh(obj.data)
152 | # bm.free()
153 | else:
154 | self.report({"WARNING"}, context.object.name + " is not a MESH.")
155 |
156 | return {"FINISHED"}
157 |
158 | def draw(self, context):
159 | layout = self.layout
160 | col = layout.column(align=True)
161 | col.prop(
162 | context.scene.IOPS,
163 | "iops_vertex_color",
164 | text="",
165 | )
166 | # TODO: Add use active color
167 | # col.prop(self, "use_active_color", text="Use Active Color")
168 | col.prop(self, "col_attr_name", text="Color Attribute Name")
169 | col.prop(self, "attr_type", text="Attribute Type")
170 | col.prop(self, "domain", text="Domain")
171 | col.prop(self, "fill_color_black", text="Fill Black")
172 | col.prop(self, "fill_color_grey", text="Fill Grey")
173 | col.prop(self, "fill_color_white", text="Fill White")
174 |
175 |
176 | class IOPS_OT_VertexColorAlphaAssign(bpy.types.Operator):
177 | """Assign Vertex Color Alpha to selected vertecies"""
178 |
179 | bl_idname = "iops.mesh_assign_vertex_color_alpha"
180 | bl_label = "Assign Vertex Color Alpha to selected vertecies"
181 | bl_options = {"REGISTER", "UNDO"}
182 |
183 | vertex_color_alpha: FloatProperty(
184 | name="Alpha",
185 | description="Alpha channel value. 0 - Transparent, 1 - Solid",
186 | default=1.0,
187 | min=0.0,
188 | max=1.0,
189 | )
190 |
191 | @classmethod
192 | def poll(cls, context):
193 | return (
194 | context.object
195 | and context.object.type == "MESH"
196 | and context.object.mode == "EDIT"
197 | )
198 |
199 | def execute(self, context):
200 | if context.object.mode == "EDIT":
201 | bpy.ops.object.editmode_toggle()
202 |
203 | mesh = bpy.context.active_object.data
204 | if mesh.vertex_colors[:] == []:
205 | mesh.vertex_colors.new()
206 | vertices = mesh.vertices
207 | vcol = mesh.vertex_colors.active
208 |
209 | for loop_index, loop in enumerate(mesh.loops):
210 | # If vertex selected
211 | if vertices[loop.vertex_index].select:
212 | vertex_color = vcol.data[loop_index].color
213 | vertex_color[3] = self.vertex_color_alpha
214 | mesh.update()
215 | bpy.ops.object.editmode_toggle()
216 | return {"FINISHED"}
217 |
218 | def draw(self, context):
219 | layout = self.layout
220 | row = layout.row()
221 | row.prop(self, "vertex_color_alpha", slider=True, text="Alpha value")
222 |
--------------------------------------------------------------------------------
/operators/cursor_origin/mesh.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import blf
3 | import gpu
4 | from gpu_extras.batch import batch_for_shader
5 | from mathutils import Vector
6 | from ..iops import IOPS_OT_Main
7 |
8 |
9 | # ---------------------------- UI ---------------------------------------
10 | def draw_line_cursor(self, context):
11 | coords = self.gpu_verts
12 | shader = gpu.shader.from_builtin("UNIFORM_COLOR")
13 | batch = batch_for_shader(shader, "LINES", {"pos": coords})
14 | shader.bind()
15 | shader.uniform_float("color", (0.1, 0.6, 0.4, 1))
16 | batch.draw(shader)
17 | # pass
18 |
19 |
20 | def draw_ui(self, context, _uidpi, _uifactor):
21 |
22 | def get_target():
23 | if self.target == context.scene.cursor:
24 | return "3D Cursor"
25 | elif self.target == context.view_layer.objects.active:
26 | return "Active object"
27 |
28 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
29 | tColor = prefs.text_color
30 | tKColor = prefs.text_color_key
31 | tCSize = prefs.text_size
32 | tCPosX = prefs.text_pos_x
33 | tCPosY = prefs.text_pos_y
34 | tShadow = prefs.text_shadow_toggle
35 | tSColor = prefs.text_shadow_color
36 | tSBlur = prefs.text_shadow_blur
37 | tSPosX = prefs.text_shadow_pos_x
38 | tSPosY = prefs.text_shadow_pos_y
39 |
40 | _target = get_target()
41 | iops_text = (
42 | ("Look at", str(_target)),
43 | ("Look at axis", str(self.look_axis[0])),
44 | ("Match cursor's rotation", str(self.rotate)),
45 | ("Align to cursor's pos", "F3"),
46 | ("Visual origin helper", "F4"),
47 | )
48 |
49 | # FontID
50 | font = 0
51 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
52 | blf.size(font, tCSize)
53 | if tShadow:
54 | blf.enable(font, blf.SHADOW)
55 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3])
56 | blf.shadow_offset(font, tSPosX, tSPosY)
57 | else:
58 | blf.disable(0, blf.SHADOW)
59 |
60 | textsize = tCSize
61 | # get leftbottom corner
62 | offset = tCPosY
63 | columnoffs = (textsize * 15) * _uifactor
64 | for line in reversed(iops_text):
65 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
66 | blf.position(font, tCPosX * _uifactor, offset, 0)
67 | blf.draw(font, line[0])
68 |
69 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3])
70 | textdim = blf.dimensions(0, line[1])
71 | coloffset = columnoffs - textdim[0] + tCPosX
72 | blf.position(0, coloffset, offset, 0)
73 | blf.draw(font, line[1])
74 | offset += (tCSize + 5) * _uifactor
75 |
76 |
77 | class IOPS_OT_CursorOrigin_Mesh(IOPS_OT_Main):
78 | bl_idname = "iops.cursor_origin_mesh"
79 | bl_label = "MESH: Object mode - Align to cursor"
80 | orig_mxs = []
81 | rotate = False
82 | flip = False
83 | target = None
84 | look_axis = []
85 | gpu_verts = []
86 |
87 | @classmethod
88 | def poll(self, context):
89 | return (
90 | context.area.type == "VIEW_3D"
91 | and context.mode == "OBJECT"
92 | and context.view_layer.objects.active.type in {"MESH", "LIGHT"}
93 | and context.view_layer.objects.selected[:] != []
94 | )
95 |
96 | def move_to_cursor(self, rotate):
97 | scene = bpy.context.scene
98 | objs = bpy.context.selected_objects
99 | for ob in objs:
100 | ob.location = scene.cursor.location
101 | if rotate:
102 | ob.rotation_euler = scene.cursor.rotation_euler
103 |
104 | def look_at(self, context, target, axis, flip):
105 | objs = bpy.context.selected_objects
106 | self.gpu_verts = []
107 |
108 | for o in objs:
109 | # Reset matrix
110 | q = o.matrix_world.to_quaternion()
111 | m = q.to_matrix()
112 | m = m.to_4x4()
113 | o.matrix_world @= m.inverted()
114 |
115 | self.gpu_verts.append(o.location)
116 | self.gpu_verts.append(target.location)
117 |
118 | v = Vector(o.location - target.location)
119 | if flip:
120 | rot_mx = v.to_track_quat("-" + axis[0], axis[1]).to_matrix().to_4x4()
121 | else:
122 | rot_mx = v.to_track_quat(axis[0], axis[1]).to_matrix().to_4x4()
123 | o.matrix_world @= rot_mx
124 |
125 | def modal(self, context, event):
126 | context.area.tag_redraw()
127 | objs = context.selected_objects
128 |
129 | if event.type in {"MIDDLEMOUSE"}:
130 | # Allow navigation
131 | return {"PASS_THROUGH"}
132 |
133 | elif event.type == "F4" and event.value == "PRESS":
134 | bpy.ops.iops.object_visual_origin("INVOKE_DEFAULT")
135 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_cursor, "WINDOW")
136 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW")
137 | return {"FINISHED"}
138 |
139 | elif event.type == "F1" and event.value == "PRESS":
140 | self.flip = not self.flip
141 | self.target = context.scene.cursor
142 | self.look_at(context, self.target, self.look_axis, self.flip)
143 | self.report({"INFO"}, event.type)
144 |
145 | elif event.type == "F2" and event.value == "PRESS":
146 | self.flip = not self.flip
147 | self.target = context.view_layer.objects.active
148 | self.look_at(context, self.target, self.look_axis, self.flip)
149 | self.report({"INFO"}, event.type)
150 |
151 | elif event.type == "X" and event.value == "PRESS":
152 | self.flip = not self.flip
153 | self.look_axis = [("X"), ("Z")]
154 | self.look_at(context, self.target, self.look_axis, self.flip)
155 | self.report({"INFO"}, event.type)
156 |
157 | elif event.type == "Y" and event.value == "PRESS":
158 | self.flip = not self.flip
159 | self.look_axis = [("Y"), ("X")]
160 | self.look_at(context, self.target, self.look_axis, self.flip)
161 | self.report({"INFO"}, event.type)
162 |
163 | elif event.type == "Z" and event.value == "PRESS":
164 | self.flip = not self.flip
165 | self.look_axis = [("Z"), ("Y")]
166 | self.look_at(context, self.target, self.look_axis, self.flip)
167 | self.report({"INFO"}, event.type)
168 |
169 | elif event.type == "F3" and event.value == "PRESS":
170 | for o, m in zip(objs, self.orig_mxs):
171 | o.matrix_world = m
172 | self.rotate = not self.rotate
173 | self.move_to_cursor(self.rotate)
174 | self.report({"INFO"}, event.type)
175 |
176 | elif event.type in {"LEFTMOUSE", "SPACE"} and event.value == "PRESS":
177 | # self.execute(context)
178 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_cursor, "WINDOW")
179 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW")
180 | return {"FINISHED"}
181 |
182 | elif event.type in {"RIGHTMOUSE", "ESC"}:
183 | for o, m in zip(objs, self.orig_mxs):
184 | o.matrix_world = m
185 | # clean up
186 | self.orig_mxs = []
187 |
188 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_cursor, "WINDOW")
189 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW")
190 | return {"CANCELLED"}
191 |
192 | return {"RUNNING_MODAL"}
193 |
194 | def invoke(self, context, event):
195 | self.orig_mxs = []
196 | self.gpu_verts = []
197 | self.look_axis = [("Z"), ("Y")]
198 | self.target = context.scene.cursor
199 | objs = context.selected_objects
200 | preferences = context.preferences
201 | # Store matricies for undo
202 | for o in objs:
203 | self.orig_mxs.append(o.matrix_world.copy())
204 |
205 | if context.object and context.area.type == "VIEW_3D":
206 | # Add drawing handler for text overlay rendering
207 | uidpi = int((72 * preferences.system.ui_scale))
208 | args = (self, context, uidpi, preferences.system.ui_scale)
209 | self._handle_ui = bpy.types.SpaceView3D.draw_handler_add(
210 | draw_ui, args, "WINDOW", "POST_PIXEL"
211 | )
212 | args_line = (self, context)
213 | self._handle_cursor = bpy.types.SpaceView3D.draw_handler_add(
214 | draw_line_cursor, args_line, "WINDOW", "POST_VIEW"
215 | )
216 |
217 | # Add modal handler to enter modal mode
218 | context.window_manager.modal_handler_add(self)
219 | return {"RUNNING_MODAL"}
220 | else:
221 | self.report({"WARNING"}, "No active object, could not finish")
222 | return {"CANCELLED"}
223 |
--------------------------------------------------------------------------------
/operators/cursor_rotate.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.props import BoolProperty, FloatProperty, EnumProperty
3 | import math
4 | import mathutils
5 |
6 | # Cursor Rotate Operator
7 | class IOPS_OT_Cursor_Rotate(bpy.types.Operator):
8 | """Rotate cursor around X or Y or Z axis"""
9 | bl_idname = "iops.cursor_rotate"
10 | bl_label = "Cursor Rotate"
11 | bl_options = {"REGISTER", "UNDO"}
12 |
13 | reverse: BoolProperty(
14 | name="reverse", description="Reverse rotation", default=False
15 | )
16 | angle: FloatProperty(
17 | name="angle", description="Rotation angle", default=90
18 | )
19 | rotation_axis: EnumProperty(
20 | name="Axis",
21 | description="Rotation axis",
22 | items=[
23 | ("X", "X", "", "", 0),
24 | ("Y", "Y", "", "", 1),
25 | ("Z", "Z", "", "", 2),
26 | ],
27 | default="X",
28 | )
29 |
30 | @classmethod
31 | def poll(cls, context):
32 | return context.area.type == "VIEW_3D"
33 |
34 | def execute(self, context):
35 | cursor = bpy.context.scene.cursor
36 | mx = cursor.matrix
37 | # rotate cursor using maxtrix
38 | match self.rotation_axis:
39 | case 'X':
40 | if self.reverse:
41 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(-self.angle), 4, 'X')
42 | else:
43 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(self.angle), 4, 'X')
44 | case 'Y':
45 | if self.reverse:
46 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(-self.angle), 4, 'Y')
47 | else:
48 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(self.angle), 4, 'Y')
49 | case 'Z':
50 | if self.reverse:
51 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(-self.angle), 4, 'Z')
52 | else:
53 | cursor.matrix = mx @ mathutils.Matrix.Rotation(math.radians(self.angle), 4, 'Z')
54 | # Report
55 | self.report({'INFO'}, f'Cursor rotated {self.angle} around {self.rotation_axis} axis')
56 | return {'FINISHED'}
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/operators/curve_spline_type.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import blf
3 | from bpy.props import BoolProperty
4 |
5 |
6 | def draw_iops_curve_spline_types_text_px(self, context, _uidpi, _uifactor):
7 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
8 | tColor = prefs.text_color
9 | tKColor = prefs.text_color_key
10 | tCSize = prefs.text_size
11 | tCPosX = prefs.text_pos_x
12 | tCPosY = prefs.text_pos_y
13 | tShadow = prefs.text_shadow_toggle
14 | tSColor = prefs.text_shadow_color
15 | tSBlur = prefs.text_shadow_blur
16 | tSPosX = prefs.text_shadow_pos_x
17 | tSPosY = prefs.text_shadow_pos_y
18 |
19 | iops_text = (
20 | ("Present type is", str(self.curv_spline_type)),
21 | ("Handles state", str(self.handles)),
22 | ("Enable/Disable handles", "H"),
23 | ("Spline type POLY", "F1"),
24 | ("Spline type BEZIER", "F2"),
25 | ("Spline type NURBS", "F3"),
26 | )
27 |
28 | # FontID
29 | font = 0
30 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
31 | blf.size(font, tCSize)
32 | if tShadow:
33 | blf.enable(font, blf.SHADOW)
34 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3])
35 | blf.shadow_offset(font, tSPosX, tSPosY)
36 | else:
37 | blf.disable(0, blf.SHADOW)
38 |
39 | textsize = tCSize
40 | # get leftbottom corner
41 | offset = tCPosY
42 | columnoffs = (textsize * 13) * _uifactor
43 | for line in reversed(iops_text):
44 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
45 | blf.position(font, tCPosX * _uifactor, offset, 0)
46 | blf.draw(font, line[0])
47 |
48 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3])
49 | textdim = blf.dimensions(0, line[1])
50 | coloffset = columnoffs - textdim[0] + tCPosX
51 | blf.position(0, coloffset, offset, 0)
52 | blf.draw(font, line[1])
53 | offset += (tCSize + 5) * _uifactor
54 |
55 |
56 | class IOPS_OT_CurveSplineType(bpy.types.Operator):
57 | """Curve select spline type"""
58 |
59 | bl_idname = "iops.curve_spline_type"
60 | bl_label = "CURVE: Spline type"
61 | bl_options = {"REGISTER", "UNDO"}
62 |
63 | handles: BoolProperty(name="Use handles", description="Use handles", default=False)
64 |
65 | spl_type = []
66 | curv_spline_type = []
67 |
68 | @classmethod
69 | def poll(self, context):
70 | return (
71 | len(context.view_layer.objects.selected) != 0
72 | and context.view_layer.objects.active.type == "CURVE"
73 | and context.view_layer.objects.active.mode == "EDIT"
74 | )
75 |
76 | def get_curve_active_spline_type(self, context):
77 | curve = context.view_layer.objects.active.data
78 | active_spline_type = curve.splines.active.type
79 | return active_spline_type
80 |
81 | def execute(self, context):
82 | bpy.ops.curve.spline_type_set(type=self.spl_type, use_handles=self.handles)
83 | return {"FINISHED"}
84 |
85 | def modal(self, context, event):
86 | context.area.tag_redraw()
87 |
88 | if event.type in {"MIDDLEMOUSE", "WHEELDOWNMOUSE", "WHEELUPMOUSE"}:
89 | # Allow navigation
90 | return {"PASS_THROUGH"}
91 |
92 | elif event.type in {"F1"} and event.value == "PRESS":
93 | self.spl_type = "POLY"
94 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW")
95 | self.execute(context)
96 | return {"FINISHED"}
97 |
98 | elif event.type in {"F2"} and event.value == "PRESS":
99 | self.spl_type = "BEZIER"
100 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW")
101 | self.execute(context)
102 | return {"FINISHED"}
103 |
104 | elif event.type in {"F3"} and event.value == "PRESS":
105 | self.spl_type = "NURBS"
106 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW")
107 | self.execute(context)
108 | return {"FINISHED"}
109 |
110 | elif event.type in {"H"} and event.value == "PRESS":
111 | hnd = self.handles
112 | if hnd:
113 | self.handles = False
114 | else:
115 | self.handles = True
116 |
117 | elif event.type in {"RIGHTMOUSE", "ESC"}:
118 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_text, "WINDOW")
119 | return {"CANCELLED"}
120 | return {"RUNNING_MODAL"}
121 |
122 | def invoke(self, context, event):
123 | preferences = context.preferences
124 | if context.object and context.area.type == "VIEW_3D":
125 | self.handles = False
126 | self.spl_type = "POLY"
127 | self.curv_spline_type = self.get_curve_active_spline_type(context)
128 | # Add drawing handler for text overlay rendering
129 | uidpi = int((72 * preferences.system.ui_scale))
130 | args = (self, context, uidpi, preferences.system.ui_scale)
131 | self._handle_text = bpy.types.SpaceView3D.draw_handler_add(
132 | draw_iops_curve_spline_types_text_px, args, "WINDOW", "POST_PIXEL"
133 | )
134 | # Add modal handler to enter modal mode
135 | context.window_manager.modal_handler_add(self)
136 | return {"RUNNING_MODAL"}
137 | else:
138 | self.report({"WARNING"}, "No active object, could not finish")
139 | return {"CANCELLED"}
140 |
141 |
142 | def register():
143 | bpy.utils.register_class(IOPS_OT_CurveSplineType)
144 |
145 |
146 | def unregister():
147 | bpy.utils.unregister_class(IOPS_OT_CurveSplineType)
148 |
149 |
150 | if __name__ == "__main__":
151 | register()
152 |
--------------------------------------------------------------------------------
/operators/curve_subdivide.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import gpu
3 | from gpu_extras.batch import batch_for_shader
4 | import blf
5 | from bpy.props import (
6 | IntProperty
7 | )
8 |
9 | def draw_curve_pts(self, context):
10 | coords = self.get_curve_pts() # <- sequence
11 | shader = gpu.shader.from_builtin("UNIFORM_COLOR")
12 | batch = batch_for_shader(shader, "POINTS", {"pos": coords})
13 | shader.bind()
14 | shader.uniform_float("color", (1, 0, 0, 1))
15 | batch.draw(shader)
16 | # pass
17 |
18 |
19 | def draw_ui(self, context, _uidpi, _uifactor):
20 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
21 | tColor = prefs.text_color
22 | tKColor = prefs.text_color_key
23 | tCSize = prefs.text_size
24 | tCPosX = prefs.text_pos_x
25 | tCPosY = prefs.text_pos_y
26 | tShadow = prefs.text_shadow_toggle
27 | tSColor = prefs.text_shadow_color
28 | tSBlur = prefs.text_shadow_blur
29 | tSPosX = prefs.text_shadow_pos_x
30 | tSPosY = prefs.text_shadow_pos_y
31 |
32 | iops_text = (("Number of cuts", str(self.points_num)),)
33 | # FontID
34 | font = 0
35 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
36 | blf.size(font, tCSize)
37 | if tShadow:
38 | blf.enable(font, blf.SHADOW)
39 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3])
40 | blf.shadow_offset(font, tSPosX, tSPosY)
41 | else:
42 | blf.disable(0, blf.SHADOW)
43 |
44 | textsize = tCSize
45 | # get leftbottom corner
46 | offset = tCPosY
47 | columnoffs = (textsize * 9) * _uifactor
48 | for line in reversed(iops_text):
49 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
50 | blf.position(font, tCPosX * _uifactor, offset, 0)
51 | blf.draw(font, line[0])
52 |
53 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3])
54 | textdim = blf.dimensions(0, line[1])
55 | coloffset = columnoffs - textdim[0] + tCPosX
56 | blf.position(0, coloffset, offset, 0)
57 | blf.draw(font, line[1])
58 | offset += (tCSize + 5) * _uifactor
59 |
60 |
61 | class IOPS_OT_CurveSubdivide(bpy.types.Operator):
62 | """Subdivide Curve"""
63 |
64 | bl_idname = "iops.curve_subdivide"
65 | bl_label = "CURVE: Subdivide"
66 | bl_options = {"REGISTER", "UNDO"}
67 |
68 | pairs = []
69 |
70 | points_num: IntProperty(name="Number of cuts", description="", default=1)
71 |
72 | @classmethod
73 | def poll(self, context):
74 | return (
75 | context.view_layer.objects.active.type == "CURVE"
76 | and context.view_layer.objects.active.mode == "EDIT"
77 | )
78 |
79 | def execute(self, context):
80 | self.subdivide(self.points_num)
81 | return {"FINISHED"}
82 |
83 | def subdivide(self, points):
84 | self.points_num = points
85 | bpy.ops.curve.subdivide(number_cuts=self.points_num)
86 |
87 | def get_curve_pts(self):
88 | obj = bpy.context.view_layer.objects.active
89 | pts = []
90 | sequence = []
91 |
92 | # Store selected curve points
93 | if obj.type == "CURVE":
94 | for s in obj.data.splines:
95 | for b in s.bezier_points:
96 | if b.select_control_point:
97 | pts.append(b)
98 |
99 | # P = (0.5)**3 * A + 3(0.5)**3 * Ha + 3(0.5) ** 3 * Hb + 0.5**3 * B
100 |
101 | for idx in range(len(pts) - 1):
102 | A = pts[idx].co @ obj.matrix_world + obj.location
103 | # Ahl = pts[idx].handle_left @ obj.matrix_world + obj.location
104 | Ahr = pts[idx].handle_right @ obj.matrix_world + obj.location # Ha
105 |
106 | B = pts[idx + 1].co @ obj.matrix_world + obj.location
107 | Bhl = pts[idx + 1].handle_left @ obj.matrix_world + obj.location # Hb
108 | # Bhr = pts[idx + 1].handle_right @ obj.matrix_world + obj.location
109 |
110 | for ip in range(self.points_num):
111 | p = 1 / (self.points_num + 1) * (ip + 1)
112 | point = (
113 | (1 - p) ** 3 * A
114 | + 3 * (1 - p) ** 2 * p * Ahr
115 | + 3 * (1 - p) * p**2 * Bhl
116 | + p**3 * B
117 | )
118 | sequence.append(point)
119 | return sequence
120 |
121 | def modal(self, context, event):
122 | context.area.tag_redraw()
123 |
124 | if event.type in {"MIDDLEMOUSE"}:
125 | # Allow navigation
126 | return {"PASS_THROUGH"}
127 |
128 | elif event.type == "WHEELDOWNMOUSE":
129 | if self.points_num > 1:
130 | self.points_num -= 1
131 | self.report({"INFO"}, event.type)
132 |
133 | elif event.type == "WHEELUPMOUSE":
134 | self.points_num += 1
135 | self.report({"INFO"}, event.type)
136 |
137 | elif event.type in {"LEFTMOUSE", "SPACE"} and event.value == "PRESS":
138 | self.execute(context)
139 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_curve, "WINDOW")
140 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW")
141 | return {"FINISHED"}
142 |
143 | elif event.type in {"RIGHTMOUSE", "ESC"}:
144 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_ui, "WINDOW")
145 | bpy.types.SpaceView3D.draw_handler_remove(self._handle_curve, "WINDOW")
146 | return {"CANCELLED"}
147 |
148 | return {"RUNNING_MODAL"}
149 |
150 | def invoke(self, context, event):
151 | preferences = context.preferences
152 | if context.object and context.area.type == "VIEW_3D":
153 | self.points_num = 1
154 |
155 | # Add drawing handler for text overlay rendering
156 | uidpi = int((72 * preferences.system.ui_scale))
157 | args = (self, context, uidpi, preferences.system.ui_scale)
158 | self._handle_ui = bpy.types.SpaceView3D.draw_handler_add(
159 | draw_ui, args, "WINDOW", "POST_PIXEL"
160 | )
161 | args_line = (self, context)
162 | self._handle_curve = bpy.types.SpaceView3D.draw_handler_add(
163 | draw_curve_pts, args_line, "WINDOW", "POST_VIEW"
164 | )
165 | self.pairs = self.get_curve_pts()
166 | # Add modal handler to enter modal mode
167 | context.window_manager.modal_handler_add(self)
168 | return {"RUNNING_MODAL"}
169 | else:
170 | self.report({"WARNING"}, "No active object, could not finish")
171 | return {"CANCELLED"}
172 |
--------------------------------------------------------------------------------
/operators/drag_snap_cursor.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import blf
3 |
4 |
5 | def draw_iops_text(self, context, _uidpi, _uifactor):
6 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
7 | tColor = prefs.text_color
8 | tKColor = prefs.text_color_key
9 | tCSize = prefs.text_size
10 | tCPosX = prefs.text_pos_x
11 | tCPosY = prefs.text_pos_y
12 | tShadow = prefs.text_shadow_toggle
13 | tSColor = prefs.text_shadow_color
14 | tSBlur = prefs.text_shadow_blur
15 | tSPosX = prefs.text_shadow_pos_x
16 | tSPosY = prefs.text_shadow_pos_y
17 |
18 | iops_text = (
19 | ("Press and Hold", "Q"),
20 | ("Pick snapping points", "Left Mouse Button Click"),
21 | )
22 |
23 | # FontID
24 | font = 0
25 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
26 | blf.size(font, tCSize)
27 | if tShadow:
28 | blf.enable(font, blf.SHADOW)
29 | blf.shadow(font, int(tSBlur), tSColor[0], tSColor[1], tSColor[2], tSColor[3])
30 | blf.shadow_offset(font, tSPosX, tSPosY)
31 | else:
32 | blf.disable(0, blf.SHADOW)
33 |
34 | textsize = tCSize
35 | # get leftbottom corner
36 | offset = tCPosY
37 | columnoffs = (textsize * 21) * _uifactor
38 | for line in reversed(iops_text):
39 | blf.color(font, tColor[0], tColor[1], tColor[2], tColor[3])
40 | blf.position(font, tCPosX * _uifactor, offset, 0)
41 | blf.draw(font, line[0])
42 |
43 | blf.color(font, tKColor[0], tKColor[1], tKColor[2], tKColor[3])
44 | textdim = blf.dimensions(0, line[1])
45 | coloffset = columnoffs - textdim[0] + tCPosX
46 | blf.position(0, coloffset, offset, 0)
47 | blf.draw(font, line[1])
48 | offset += (tCSize + 5) * _uifactor
49 |
50 |
51 | class IOPS_OT_DragSnapCursor(bpy.types.Operator):
52 | """Quick drag & snap using 3D Cursor"""
53 |
54 | bl_idname = "iops.object_drag_snap_cursor"
55 | bl_label = "IOPS Drag Snap Cursor"
56 | bl_description = (
57 | "Hold Q and LMB Click to quickly snap point to point using 3D Cursor"
58 | )
59 | bl_options = {"REGISTER", "UNDO"}
60 |
61 | step = 1
62 | count = 0
63 | old_type = None
64 | old_value = None
65 |
66 | def clear_draw_handlers(self):
67 | for handler in self.vp_handlers:
68 | bpy.types.SpaceView3D.draw_handler_remove(handler, "WINDOW")
69 |
70 | @classmethod
71 | def poll(cls, context):
72 | return (
73 | context.area.type == "VIEW_3D"
74 | and context.mode == "OBJECT"
75 | and len(context.view_layer.objects.selected) != 0
76 | )
77 |
78 | def modal(self, context, event):
79 | context.area.tag_redraw()
80 | # prevent spamming
81 | # new_type = event.type
82 | # new_value = event.value
83 | # if new_type != self.old_type and new_value != self.old_value:
84 | # print(event.type, event.value)
85 | # self.old_type = new_type
86 | # self.old_value = new_value
87 |
88 | if (
89 | event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}
90 | and event.value == "PRESS"
91 | ):
92 | return {"PASS_THROUGH"}
93 | elif event.type in {"ESC", "RIGHMOUSE"} and event.value == "PRESS":
94 | try:
95 | self.clear_draw_handlers()
96 | except ValueError:
97 | pass
98 | return {"CANCELLED"}
99 |
100 | elif event.type == "Q" and event.value == "PRESS":
101 | print("Count:", self.count)
102 | bpy.ops.transform.translate(
103 | "INVOKE_DEFAULT",
104 | cursor_transform=True,
105 | use_snap_self=True,
106 | snap_target="CLOSEST",
107 | use_snap_nonedit=True,
108 | snap_elements={"VERTEX"},
109 | snap=True,
110 | release_confirm=True,
111 | )
112 | self.count += 1
113 | if self.count == 1:
114 | # print("Count:", 1)
115 | self.report({"INFO"}, "Step 2: Q to place cursor at point B")
116 | elif self.count == 2:
117 | bpy.context.scene.IOPS.dragsnap_point_a = (
118 | bpy.context.scene.cursor.location
119 | )
120 | # print("Count:", 2)
121 | self.report({"INFO"}, "Step 3: press Q")
122 | elif self.count == 3:
123 | # print("Count:", 3)
124 | bpy.context.scene.IOPS.dragsnap_point_b = (
125 | bpy.context.scene.cursor.location
126 | )
127 | vector = (
128 | bpy.context.scene.IOPS.dragsnap_point_b
129 | - bpy.context.scene.IOPS.dragsnap_point_a
130 | )
131 | bpy.ops.transform.translate(value=vector, orient_type="GLOBAL")
132 | try:
133 | self.clear_draw_handlers()
134 | except ValueError:
135 | pass
136 | return {"FINISHED"}
137 |
138 | return {"RUNNING_MODAL"}
139 |
140 | def invoke(self, context, event):
141 | preferences = context.preferences
142 | if context.space_data.type == "VIEW_3D":
143 | uidpi = int((72 * preferences.system.ui_scale))
144 | args_text = (self, context, uidpi, preferences.system.ui_scale)
145 | # Add draw handlers
146 | self._handle_iops_text = bpy.types.SpaceView3D.draw_handler_add(
147 | draw_iops_text, args_text, "WINDOW", "POST_PIXEL"
148 | )
149 | self.report({"INFO"}, "Step 1: Q to place cursor at point A")
150 | self.vp_handlers = [self._handle_iops_text]
151 | # Add modal handler to enter modal mode
152 | context.window_manager.modal_handler_add(self)
153 | return {"RUNNING_MODAL"}
154 | else:
155 | self.report({"WARNING"}, "Active space must be a View3d")
156 | return {"CANCELLED"}
157 |
--------------------------------------------------------------------------------
/operators/easy_mod_curve.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.props import (
3 | BoolProperty,
4 | EnumProperty,
5 | )
6 |
7 |
8 | class IOPS_OT_Easy_Mod_Curve(bpy.types.Operator):
9 | """Select picked curve in curve modifier"""
10 |
11 | bl_idname = "iops.modifier_easy_curve"
12 | bl_label = "Easy Modifier - Curve"
13 | bl_options = {"REGISTER", "UNDO"}
14 |
15 | use_curve_radius: BoolProperty(
16 | name="Use Curve Radius",
17 | description="Causes the deformed object to be scaled by the set curve radius.",
18 | default=True,
19 | )
20 | use_curve_stretch: BoolProperty(
21 | name="Use Curve Length",
22 | description="The Stretch curve option allows you to let the mesh object stretch, or squeeze, over the entire curve.",
23 | default=True,
24 | )
25 | use_curve_bounds_clamp: BoolProperty(
26 | name="Use Curve Bounds",
27 | description="When this option is enabled, the object and mesh offset along the deformation axis is ignored.",
28 | default=True,
29 | )
30 |
31 | curve_modifier_axis: EnumProperty(
32 | name="Deformation Axis",
33 | description="Deformation along selected axis",
34 | items=[
35 | ("POS_X", "X", "", "", 0),
36 | ("POS_Y", "Y", "", "", 1),
37 | ("POS_Z", "Z", "", "", 2),
38 | ("NEG_X", "-X", "", "", 3),
39 | ("NEG_Y", "-Y", "", "", 4),
40 | ("NEG_Z", "-Z", "", "", 5),
41 | ],
42 | default="POS_X",
43 | )
44 | find_array_and_set_curve_fit: BoolProperty(
45 | name="Array - Fit Curve",
46 | description="Find Array modifier in the active object modifier list and set Array Fit type to - Fit Curve and pick selected spline there",
47 | default=False,
48 | )
49 | Replace_Curve_Modifier: BoolProperty(
50 | name="Find and Replace Curve Modifier",
51 | description="Find and replace iOps curve modifier",
52 | default=True,
53 | )
54 |
55 | @classmethod
56 | def poll(cls, context):
57 | return context.mode == "OBJECT" and context.area.type == "VIEW_3D"
58 |
59 | def execute(self, context):
60 | if (
61 | len(context.view_layer.objects.selected) == 1
62 | and context.active_object.type == "MESH"
63 | ):
64 | for mod in bpy.context.active_object.modifiers:
65 | if mod.type == "CURVE":
66 | bpy.ops.object.select_all(action="DESELECT")
67 | mod.object.select_set(True)
68 | context.view_layer.objects.active = mod.object
69 | self.report({"INFO"}, "Curve Modifer - Object selected.")
70 |
71 | if len(context.view_layer.objects.selected) == 2:
72 | obj = None
73 | curve = None
74 |
75 | for ob in context.view_layer.objects.selected:
76 | if ob.type == "MESH":
77 | obj = ob
78 | if ob.type == "CURVE":
79 | curve = ob
80 |
81 | if obj and curve:
82 | cur = context.scene.cursor
83 | curve.data.use_radius = self.use_curve_radius
84 | curve.data.use_stretch = self.use_curve_stretch
85 | curve.data.use_deform_bounds = self.use_curve_bounds_clamp
86 |
87 | if obj.location != curve.location:
88 | bpy.ops.object.select_all(action="DESELECT")
89 | curve.select_set(True)
90 | context.view_layer.objects.active = curve
91 |
92 | if curve.data.splines.active.type == "POLY":
93 | cur.location = (
94 | curve.data.splines.active.points[0].co.xyz
95 | @ curve.matrix_world.transposed()
96 | )
97 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN")
98 |
99 | if curve.data.splines.active.type == "BEZIER":
100 | cur.location = (
101 | curve.data.splines.active.bezier_points[0].co
102 | @ curve.matrix_world.transposed()
103 | )
104 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN")
105 |
106 | bpy.ops.object.select_all(action="DESELECT")
107 | obj.location = curve.location
108 |
109 | if obj.modifiers:
110 | if self.Replace_Curve_Modifier:
111 | for mod in obj.modifiers:
112 | if mod.type == "CURVE":
113 | mod.object = curve
114 | mod.deform_axis = self.curve_modifier_axis
115 | curve.select_set(True)
116 | obj.select_set(True)
117 | context.view_layer.objects.active = obj
118 | self.report({"INFO"}, "Curve Modifier Replaced")
119 | else:
120 | mod = obj.modifiers.new("iOps Curve", type="CURVE")
121 | mod.object = curve
122 | mod.deform_axis = self.curve_modifier_axis
123 | curve.select_set(True)
124 | obj.select_set(True)
125 | context.view_layer.objects.active = obj
126 | self.report(
127 | {"INFO"}, "New Curve Modifier added and curve object picked"
128 | )
129 |
130 | if self.find_array_and_set_curve_fit:
131 | for mod in obj.modifiers:
132 | if mod.type == "ARRAY":
133 | mod.fit_type = "FIT_CURVE"
134 | mod.curve = curve
135 | self.report(
136 | {"INFO"}, "Array modifier found and curve setted"
137 | )
138 | else:
139 | mod = obj.modifiers.new("iOps Curve", type="CURVE")
140 | mod.object = curve
141 | mod.deform_axis = self.curve_modifier_axis
142 | curve.select_set(True)
143 | obj.select_set(True)
144 | context.view_layer.objects.active = obj
145 | self.report(
146 | {"INFO"}, "New Curve Modifier added and curve object picked"
147 | )
148 | else:
149 | self.report({"WARNING"}, "Mesh or Curve missing!!!")
150 | return {"FINISHED"}
151 |
--------------------------------------------------------------------------------
/operators/easy_mod_shwarp.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.props import (
3 | BoolProperty,
4 | EnumProperty,
5 | FloatProperty,
6 | )
7 |
8 |
9 | class IOPS_OT_Easy_Mod_Shwarp(bpy.types.Operator):
10 | """Select picked curve in curve modifier"""
11 |
12 | bl_idname = "iops.modifier_easy_shwarp"
13 | bl_label = "Easy Modifier - Shrinkwarp"
14 | bl_options = {"REGISTER", "UNDO"}
15 |
16 | shwarp_offset: FloatProperty(
17 | name="Offset",
18 | description="Offset factor",
19 | default=0.0,
20 | min=0.00,
21 | max=9999.0,
22 | )
23 | shwarp_method: EnumProperty(
24 | name="Mode",
25 | description="Mod",
26 | items=[
27 | ("NEAREST_SURFACEPOINT", "NEAREST_SURFACEPOINT", "", "", 0),
28 | ("PROJECT", "PROJECT", "", "", 1),
29 | ("NEAREST_VERTEX", "NEAREST_VERTEX", "", "", 2),
30 | ("TARGET_PROJECT", "TARGET_PROJECT", "", "", 3),
31 | ],
32 | default="PROJECT",
33 | )
34 | shwarp_use_vg: BoolProperty(
35 | name="Use vertex groups", description="Takes last one", default=False
36 | )
37 |
38 | transfer_normals: BoolProperty(
39 | name="Transfer Normals",
40 | description="Add mod to transfer normals from target object",
41 | default=False,
42 | )
43 |
44 | stack_location: EnumProperty(
45 | name="Mod location in stack",
46 | description="Where to put SWARP modifier?",
47 | items=[
48 | ("First", "First", "", "", 0),
49 | ("Last", "Last", "", "", 1),
50 | ("Before Active", "Before Active", "", "", 2),
51 | ("After Active", "After Active", "", "", 3),
52 | ],
53 | default="Last",
54 | )
55 |
56 | @classmethod
57 | def poll(cls, context):
58 | return (
59 | context.object
60 | and context.object.type == "MESH"
61 | and context.area.type == "VIEW_3D"
62 | and len(context.view_layer.objects.selected) >= 2
63 | )
64 |
65 | def execute(self, context):
66 | target = context.active_object
67 | ctx = bpy.context.copy()
68 | objs = []
69 |
70 | for ob in context.view_layer.objects.selected:
71 | if ob.name != target.name and ob.type == "MESH":
72 | objs.append(ob)
73 |
74 | if objs and target:
75 | # print(objs)
76 | for ob in objs:
77 | if self.shwarp_use_vg and ob.vertex_groups[:] == []:
78 | self.shwarp_use_vg = False
79 | self.transfer_normals = False
80 |
81 | if ob.modifiers:
82 | mod_list = ob.modifiers.keys()
83 | active_idx = mod_list.index(ob.modifiers.active.name)
84 | print("Active mod:", ob.modifiers.active.name)
85 |
86 | # Context copy
87 | ctx["object"] = ob
88 | ctx["active_object"] = ob
89 | ctx["selected_objects"] = [ob]
90 | ctx["selected_editable_objects"] = [ob]
91 |
92 | if "iOps Shwarp" in ob.modifiers.keys():
93 | continue
94 | # mod = ob.modifiers['iOps Shwarp']
95 | # mod.show_in_editmode = True
96 | # mod.show_on_cage = True
97 | # mod.target = target
98 | # mod.offset = self.shwarp_offset
99 | # mod.wrap_method = self.shwarp_method
100 | # if self.shwarp_use_vg:
101 | # mod.vertex_group = ob.vertex_groups[0].name
102 | else:
103 | mod = ob.modifiers.new("iOps Shwarp", type="SHRINKWRAP")
104 | mod.show_in_editmode = True
105 | mod.show_on_cage = True
106 | mod.target = target
107 | mod.offset = self.shwarp_offset
108 | mod.wrap_method = self.shwarp_method
109 | if self.shwarp_use_vg and ob.vertex_groups:
110 | mod.vertex_group = ob.vertex_groups[0].name
111 |
112 | # Logic to move modifier around
113 | if self.stack_location in {"First", "Last"}:
114 | count = len(ob.modifiers) - 1
115 | elif self.stack_location == "Before Active":
116 | count = len(ob.modifiers) - 1 - active_idx
117 | elif self.stack_location == "After Active":
118 | count = len(ob.modifiers) - 2 - active_idx
119 |
120 | print("Starting moves:", count)
121 |
122 | while count > 0:
123 | if self.stack_location in {
124 | "First",
125 | "Before Active",
126 | "After Active",
127 | }:
128 | with context.temp_override(**ctx):
129 | bpy.ops.object.modifier_move_down(
130 | modifier="iOps Shwarp"
131 | )
132 | else:
133 | break
134 | count -= 1
135 | print("Active mod:", ob.modifiers.active.name, count)
136 |
137 | # Best settings for default Z projection, will use first group for snapping
138 | if self.shwarp_method == "PROJECT":
139 | mod.use_project_z = True
140 | mod.use_negative_direction = True
141 | mod.use_positive_direction = True
142 | if self.shwarp_use_vg and ob.vertex_groups:
143 | mod.vertex_group = ob.vertex_groups[0].name
144 |
145 | if self.transfer_normals:
146 | if "iOps Transfer Normals" not in ob.modifiers.keys():
147 | mod = ob.modifiers.new(
148 | "iOps Transfer Normals", type="DATA_TRANSFER"
149 | )
150 | mod.object = target
151 | mod.use_loop_data = True
152 | mod.data_types_loops = {"CUSTOM_NORMAL"}
153 | mod.loop_mapping = "POLYINTERP_NEAREST"
154 | if self.shwarp_use_vg and ob.vertex_groups:
155 | mod.vertex_group = ob.vertex_groups[0].name
156 | else:
157 | continue
158 |
159 | return {"FINISHED"}
160 |
--------------------------------------------------------------------------------
/operators/executor.py:
--------------------------------------------------------------------------------
1 | import os
2 | from os import listdir
3 | from os.path import isfile, join
4 | import bpy
5 | from bpy.props import StringProperty
6 |
7 |
8 | def get_prefix(name):
9 | if "_" in name:
10 | split = name.split("_")
11 | prefix = split[0]
12 | if prefix == prefix.upper():
13 | split.pop(0)
14 | new_name = "_".join(split)
15 | return prefix, new_name
16 | else:
17 | return prefix[0].upper(), name
18 | else:
19 | return name[0].upper(), name
20 |
21 |
22 | def get_executor_column_width(scripts):
23 | max_lenght = 0
24 | executor_name_lenght = bpy.context.preferences.addons["InteractionOps"].preferences.executor_name_lenght
25 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
26 |
27 | for script in scripts:
28 | full_name = os.path.split(script)
29 | name = os.path.splitext(full_name[1])[0]
30 | if len(name) > max_lenght:
31 | max_lenght = len(name) + executor_name_lenght
32 |
33 | min_width = executor_name_lenght
34 | column_count = max(1, int(len(scripts) / prefs.executor_column_count))
35 | return max(min_width, max_lenght)
36 |
37 |
38 | class IOPS_OT_Executor(bpy.types.Operator):
39 | """Execute python scripts from folder"""
40 |
41 | bl_idname = "iops.executor"
42 | bl_label = "IOPS Executor"
43 | bl_options = {"REGISTER"}
44 |
45 | script: StringProperty(
46 | name="Script path",
47 | default="",
48 | )
49 |
50 | def execute(self, context):
51 | filename = self.script
52 | exec(compile(open(filename).read(), filename, "exec"))
53 | return {"FINISHED"}
54 |
55 |
56 | class IOPS_PT_ExecuteList(bpy.types.Panel):
57 | bl_idname = "IOPS_PT_ExecuteList"
58 | bl_label = "Executor list"
59 | bl_space_type = "VIEW_3D"
60 | bl_region_type = "WINDOW"
61 |
62 | def draw(self, context):
63 | prefs = context.preferences.addons["InteractionOps"].preferences
64 | props = context.window_manager.IOPS_AddonProperties
65 | letter = ""
66 | scripts = bpy.context.scene.IOPS["executor_scripts"]
67 | global_column_amount = max(1, int(len(scripts) / prefs.executor_column_count))
68 |
69 | layout = self.layout
70 | if getattr(props, "iops_exec_filter", None):
71 | filtered_scripts = bpy.context.scene.IOPS.get("filtered_executor_scripts", [])
72 | scripts = filtered_scripts if props.iops_exec_filter and filtered_scripts else bpy.context.scene.IOPS["executor_scripts"]
73 | else:
74 | scripts = bpy.context.scene.IOPS["executor_scripts"]
75 | column_amount = max(1, int(len(scripts) / prefs.executor_column_count))
76 | layout.ui_units_x = get_executor_column_width(scripts) / (global_column_amount / column_amount)
77 | column_flow = layout.column_flow(columns=column_amount, align=False)
78 | column_flow.prop(props, "iops_exec_filter", text="", icon="VIEWZOOM")
79 | if scripts:
80 | for script in scripts: # Start counting from 1
81 | full_name = os.path.split(script)
82 | name = os.path.splitext(full_name[1])[0]
83 | list_name = name[0].upper()
84 | if str(list_name) != letter:
85 | column_flow.label(text=str(list_name))
86 | letter = str(list_name)
87 | column_flow.operator(
88 | "iops.executor", text=name, icon="FILE_SCRIPT"
89 | ).script = script
90 |
91 |
92 | class IOPS_OT_Call_MT_Executor(bpy.types.Operator):
93 | """Active object data(mesh) information"""
94 |
95 | bl_idname = "iops.scripts_call_mt_executor"
96 | bl_label = "IOPS Call Executor"
97 |
98 | def execute(self, context):
99 | addon_prop = context.window_manager.IOPS_AddonProperties
100 | addon_prop.iops_exec_filter = ""
101 | if "filtered_executor_scripts" in bpy.context.scene.IOPS.keys():
102 | del bpy.context.scene.IOPS["filtered_executor_scripts"]
103 | prefs = context.preferences.addons["InteractionOps"].preferences
104 | executor_scripts_folder = prefs.executor_scripts_folder
105 | scripts_folder = executor_scripts_folder # TODO: Add user scripts folder
106 | # scripts_folder = os.path.join(scripts_folder, "custom")
107 | _files = [f for f in listdir(scripts_folder) if isfile(join(scripts_folder, f))]
108 | files = [os.path.join(scripts_folder, f) for f in _files]
109 | scripts = [script for script in files if script[-2:] == "py"]
110 | bpy.context.scene["IOPS"]["executor_scripts"] = scripts
111 | bpy.ops.wm.call_panel(name="IOPS_PT_ExecuteList")
112 | return {"FINISHED"}
113 |
--------------------------------------------------------------------------------
/operators/grid_from_active.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import numpy as np
3 | from mathutils import Vector
4 |
5 |
6 | class IOPS_OT_ToGridFromActive(bpy.types.Operator):
7 | """Locations to grid from active"""
8 |
9 | bl_idname = "iops.object_to_grid_from_active"
10 | bl_label = "To Grid From Active"
11 | bl_options = {"REGISTER", "UNDO"}
12 |
13 | @classmethod
14 | def poll(self, context):
15 | return (
16 | context.area.type == "VIEW_3D"
17 | and context.mode == "OBJECT"
18 | and len(context.view_layer.objects.selected) != 0
19 | )
20 |
21 | def execute(self, context):
22 | C = bpy.context
23 | active = C.view_layer.objects.active
24 | origin = active.location
25 | objects = C.selected_objects
26 | mx_orig = active.matrix_world.copy()
27 |
28 | # Reset transforms of all
29 | bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
30 | active.matrix_world @= active.matrix_world.inverted()
31 | bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM")
32 |
33 | # Sizes
34 | size_x = active.dimensions[0]
35 | size_y = active.dimensions[1]
36 | size_z = active.dimensions[2]
37 |
38 | # Move objects to grid
39 | o_xyz = np.array([origin[0], origin[1], origin[2]], dtype=np.float32)
40 |
41 | for o in objects:
42 | print("Location difference before:", o.location - active.location)
43 |
44 | # Extracting 3D coordinates
45 | p = np.array([o.location[0], o.location[1], o.location[2]])
46 |
47 | # Calculations for X, Y, and Z axes
48 | p_prime = (
49 | np.around((p - o_xyz) / (size_x, size_y, size_z))
50 | * (size_x, size_y, size_z)
51 | + o_xyz
52 | )
53 |
54 | # Checking for close match
55 | if np.allclose(p, p_prime, rtol=1e-21, atol=1e-24):
56 | continue
57 |
58 | # Setting new location
59 | location = Vector((p_prime[0], p_prime[1], p_prime[2]))
60 | o.matrix_world @= o.matrix_world.inverted()
61 | o.location = location
62 |
63 | print("----")
64 | print("p", p)
65 | print("p_prime", p_prime)
66 |
67 | # Restore matrix for all and clear parent
68 | bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
69 | active.matrix_world = mx_orig
70 | bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM")
71 |
72 | self.report({"INFO"}, "Aligned to grid from active")
73 |
74 | return {"FINISHED"}
75 |
--------------------------------------------------------------------------------
/operators/hotkeys/load_hotkeys.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | import json
4 | from ...utils.functions import register_keymaps, unregister_keymaps
5 | from ...prefs.hotkeys_default import keys_default
6 |
7 |
8 | class IOPS_OT_LoadUserHotkeys(bpy.types.Operator):
9 | bl_idname = "iops.load_user_hotkeys"
10 | bl_label = "Load User's Hotkeys"
11 | bl_options = {"REGISTER", "UNDO"}
12 |
13 | def execute(self, context):
14 | unregister_keymaps()
15 | bpy.context.window_manager.keyconfigs.update()
16 |
17 | keys_user = []
18 |
19 | path = bpy.utils.script_path_user()
20 | user_hotkeys_file = os.path.join(
21 | path, "presets", "IOPS", "iops_hotkeys_user.py"
22 | )
23 | user_hotkeys_path = os.path.join(path, "presets", "IOPS")
24 |
25 | if os.path.exists(user_hotkeys_file):
26 | with open(user_hotkeys_file) as f:
27 | keys_user = json.load(f)
28 | else:
29 | if not os.path.exists(user_hotkeys_path):
30 | os.makedirs(user_hotkeys_path)
31 | with open(user_hotkeys_file, "w") as f:
32 | f.write("[]")
33 |
34 | register_keymaps(keys_user)
35 | print("Loaded user's hotkeys")
36 | return {"FINISHED"}
37 |
38 |
39 | class IOPS_OT_LoadDefaultHotkeys(bpy.types.Operator):
40 | bl_idname = "iops.load_default_hotkeys"
41 | bl_label = "Load Default Hotkeys"
42 | bl_options = {"REGISTER", "UNDO"}
43 |
44 | def execute(self, context):
45 | unregister_keymaps()
46 | bpy.context.window_manager.keyconfigs.update()
47 | register_keymaps(keys_default)
48 | print("Loaded default hotkeys")
49 | return {"FINISHED"}
50 |
--------------------------------------------------------------------------------
/operators/hotkeys/save_hotkeys.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | import json
4 |
5 |
6 | def save_hotkeys():
7 | path = bpy.utils.script_path_user()
8 | folder = os.path.join(path, "presets", "IOPS")
9 | user_hotkeys_file = os.path.join(path, "presets", "IOPS", "iops_hotkeys_user.py")
10 | if not os.path.exists(folder):
11 | os.makedirs(folder)
12 | with open(user_hotkeys_file, "w") as f:
13 | data = get_iops_keys()
14 | f.write("[" + ",\n".join(json.dumps(i) for i in data) + "]\n")
15 |
16 |
17 | def get_iops_keys():
18 | keys = []
19 |
20 | if bpy.app.version[0] == 2 and bpy.app.version[1] <= 92:
21 | keyconfig = bpy.context.window_manager.keyconfigs["blender addon"]
22 | elif bpy.app.version[0] == 2 and bpy.app.version[1] >= 92:
23 | keyconfig = bpy.context.window_manager.keyconfigs["Blender addon"]
24 | elif bpy.app.version[0] == 3 and bpy.app.version[1] >= 0:
25 | keyconfig = bpy.context.window_manager.keyconfigs["Blender addon"]
26 | elif bpy.app.version[0] == 4 and bpy.app.version[1] >= 0:
27 | keyconfig = bpy.context.window_manager.keyconfigs["Blender user"]
28 |
29 | for keymap in keyconfig.keymaps:
30 | if keymap:
31 | keymapItems = keymap.keymap_items
32 | toSave = tuple(
33 | item for item in keymapItems if item.idname.startswith("iops.")
34 | )
35 | for item in toSave:
36 | entry = (
37 | item.idname,
38 | item.type,
39 | item.value,
40 | item.ctrl,
41 | item.alt,
42 | item.shift,
43 | item.oskey,
44 | )
45 | keys.append(entry)
46 | for k in keys:
47 | print(k)
48 | return keys
49 |
50 |
51 | class IOPS_OT_SaveUserHotkeys(bpy.types.Operator):
52 | bl_idname = "iops.save_user_hotkeys"
53 | bl_label = "Save User's Hotkeys"
54 | bl_options = {"REGISTER", "UNDO"}
55 |
56 | def execute(self, context):
57 | save_hotkeys()
58 | print("Saved user's hotkeys")
59 | return {"FINISHED"}
60 |
--------------------------------------------------------------------------------
/operators/image_reload.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | class IOPS_OT_Reload_Images(bpy.types.Operator):
4 | """Reload images in the current blend file if they are existing."""
5 | bl_idname = "iops.reload_images"
6 | bl_label = "Reload Images"
7 | bl_description = "Reload Images in the current blend file if they are existing."
8 |
9 | @classmethod
10 | def poll(cls, context):
11 | return (bpy.data.images)
12 |
13 | def execute(self, context):
14 | for image in bpy.data.images:
15 | image.reload()
16 | self.report({'INFO'}, f"Image '{image.name}' reloaded.")
17 | return {'FINISHED'}
--------------------------------------------------------------------------------
/operators/iops.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from ..utils.iops_dict import IOPS_Dict
3 | from ..utils.functions import get_iop
4 |
5 |
6 | class IOPS_OT_Main(bpy.types.Operator):
7 | bl_idname = "iops.main"
8 | bl_label = "IOPS"
9 | bl_options = {"REGISTER", "UNDO"}
10 |
11 | # modes_3d = {0: "VERT", 1: "EDGE", 2: "FACE"}
12 | # modes_uv = {0: "VERTEX", 1: "EDGE", 2: "FACE", 3: "ISLAND"}
13 | # modes_gpen = {0: "EDIT_GPENCIL", 1: "PAINT_GPENCIL", 2: "SCULPT_GPENCIL"}
14 | # modes_curve = {0: "EDIT_CURVE"}
15 | # modes_text = {0: "EDIT_TEXT"}
16 | # modes_meta = {0: "EDIT_META"}
17 | # modes_lattice = {0: "EDIT_LATTICE"}
18 | # modes_armature = {0: "EDIT", 1: "POSE"}
19 | # supported_types = {"MESH", "CURVE", "GPENCIL", "EMPTY", "TEXT", "META", "ARMATURE", "LATTICE"}
20 |
21 | # @classmethod
22 | # def poll(cls, context):
23 | # return (bpy.context.object is not None and
24 | # bpy.context.active_object is not None)
25 |
26 | alt = False
27 | ctrl = False
28 | shift = False
29 |
30 | def get_mode_3d(self, tool_mesh):
31 | mode = ""
32 | if tool_mesh[0]:
33 | mode = "VERT"
34 | elif tool_mesh[1]:
35 | mode = "EDGE"
36 | elif tool_mesh[2]:
37 | mode = "FACE"
38 | return mode
39 |
40 | def get_modifier_state(self):
41 | modifiers = []
42 | if self.alt:
43 | modifiers.append("ALT")
44 | if self.ctrl:
45 | modifiers.append("CTRL")
46 | if self.shift:
47 | modifiers.append("SHIFT")
48 | return "_".join(modifiers) if modifiers else "NONE"
49 |
50 | def invoke(self, context, event):
51 | # Set modifier flags
52 | self.alt = event.alt
53 | self.ctrl = event.ctrl
54 | self.shift = event.shift
55 | return self.execute(context)
56 |
57 | def execute(self, context):
58 | op = self.operator
59 | event = self.get_modifier_state()
60 | if bpy.context.area:
61 | type_area = bpy.context.area.type
62 | if bpy.context.view_layer.objects.active:
63 | tool_mesh = bpy.context.scene.tool_settings.mesh_select_mode
64 | type_object = bpy.context.view_layer.objects.active.type
65 | mode_object = bpy.context.view_layer.objects.active.mode
66 | mode_mesh = self.get_mode_3d(tool_mesh)
67 | mode_uv = 'UV_' + bpy.context.tool_settings.uv_select_mode
68 | flag_uv = bpy.context.tool_settings.use_uv_select_sync
69 | if flag_uv:
70 | mode_uv = mode_mesh
71 |
72 | # Build query with current state
73 | query = (
74 | type_area,
75 | flag_uv,
76 | type_object,
77 | mode_object,
78 | mode_uv,
79 | mode_mesh,
80 | op,
81 | event,
82 | )
83 | else:
84 | query = (type_area, None, None, None, None, None, None, None)
85 |
86 | # Get and execute the function
87 | function = get_iop(IOPS_Dict.iops_dict, query)
88 | if function:
89 | function()
90 | else:
91 | self.report({"WARNING"}, "No operation defined for this context")
92 | else:
93 | self.report({"INFO"}, "Focus your mouse pointer on corresponding window.")
94 | return {"FINISHED"}
95 |
--------------------------------------------------------------------------------
/operators/library_reload.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | class IOPS_OT_Reload_Libraries(bpy.types.Operator):
4 | """Reload Libraries in the current blend file if they are existing."""
5 | bl_idname = "iops.reload_libraries"
6 | bl_label = "Reload Libraries"
7 | bl_description = "Reload Libraries in the current blend file if they are existing."
8 |
9 | @classmethod
10 | def poll(cls, context):
11 | return (bpy.data.libraries)
12 |
13 | def execute(self, context):
14 | for lib in bpy.data.libraries:
15 | lib.reload()
16 | self.report({'INFO'}, f"Library '{lib.name}' reloaded.")
17 | return {'FINISHED'}
18 |
--------------------------------------------------------------------------------
/operators/materials_from_textures.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | from bpy.props import CollectionProperty, BoolProperty
4 | from bpy_extras.io_utils import ImportHelper
5 | from bpy.types import Operator
6 |
7 |
8 | def material_name_generator(name):
9 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
10 | prefixes = prefs.texture_to_material_prefixes.split(",")
11 | suffixes = prefs.texture_to_material_suffixes.split(",")
12 | extension = name.find(".")
13 | name = name[:extension]
14 | for pref in prefixes:
15 | if name.startswith(pref):
16 | name = name[len(pref) :]
17 | for suf in suffixes:
18 | if name.endswith(suf):
19 | name = name[: -len(suf)]
20 | return name
21 |
22 |
23 | class IOPS_OT_MaterialsFromTextures(Operator, ImportHelper):
24 | """Create Materials from Selected Textures in the File Browser"""
25 |
26 | bl_idname = "iops.materials_from_textures"
27 | bl_label = "IOPS Materials from Textures"
28 |
29 | files: CollectionProperty(type=bpy.types.PropertyGroup)
30 | import_all: BoolProperty(
31 | name="Import all textures",
32 | description="Import all related textures",
33 | default=True,
34 | )
35 |
36 | def execute(self, context):
37 | dirname = os.path.dirname(self.filepath)
38 | print("DIRNAME", dirname)
39 | for f in self.files:
40 | path = os.path.join(dirname, f.name)
41 | # Create Shader/Matrial
42 | mat_name = material_name_generator(f.name)
43 | mat = bpy.data.materials.new(name=mat_name)
44 | mat.use_nodes = True
45 | bsdf = mat.node_tree.nodes["Principled BSDF"]
46 | bsdf.location = (0, 0)
47 | mat.node_tree.nodes["Material Output"].location = (350, 0)
48 | # Create Image
49 | texImage = mat.node_tree.nodes.new("ShaderNodeTexImage")
50 | texImage.image = bpy.data.images.load(path)
51 | texImage.image.use_fake_user = True
52 | texImage.location = (-600, 120)
53 | mat.node_tree.links.new(
54 | bsdf.inputs["Base Color"], texImage.outputs["Color"]
55 | )
56 | mat.use_fake_user = True
57 | offset = -350
58 | if self.import_all:
59 | directory = os.listdir(dirname)
60 | for file in directory:
61 | if mat_name in file and file != f.name:
62 | normal_map = mat_name + "_nm"
63 | mask_map = mat_name + "_mk"
64 | if normal_map in file:
65 | path = os.path.join(dirname, file)
66 | NormalImage = mat.node_tree.nodes.new("ShaderNodeTexImage")
67 | NormalImage.name = normal_map
68 | NormalImage.image = bpy.data.images.load(path)
69 | NormalImage.image.colorspace_settings.name = "Non-Color"
70 | NormalImage.image.use_fake_user = True
71 | NormalImage.location = (-600, -720)
72 |
73 | NormalMap = mat.node_tree.nodes.new("ShaderNodeNormalMap")
74 | NormalMap.location = (-250, -600)
75 |
76 | mat.node_tree.links.new(
77 | NormalMap.inputs["Color"], NormalImage.outputs["Color"]
78 | )
79 | mat.node_tree.links.new(
80 | bsdf.inputs["Normal"], NormalMap.outputs["Normal"]
81 | )
82 | elif mask_map in file:
83 | path = os.path.join(dirname, file)
84 | MaskImage = mat.node_tree.nodes.new("ShaderNodeTexImage")
85 | MaskImage.name = mask_map
86 | MaskImage.image = bpy.data.images.load(path)
87 | MaskImage.image.colorspace_settings.name = "Non-Color"
88 | MaskImage.image.use_fake_user = True
89 | MaskImage.location = (-600, -300)
90 |
91 | SeparateRGB = mat.node_tree.nodes.new(
92 | "ShaderNodeSeparateRGB"
93 | )
94 | SeparateRGB.location = (-250, -250)
95 |
96 | mat.node_tree.links.new(
97 | SeparateRGB.inputs["Image"], MaskImage.outputs["Color"]
98 | )
99 | mat.node_tree.links.new(
100 | bsdf.inputs["Metallic"], SeparateRGB.outputs["R"]
101 | )
102 | mat.node_tree.links.new(
103 | bsdf.inputs["Roughness"], SeparateRGB.outputs["G"]
104 | )
105 | else:
106 | path = os.path.join(dirname, file)
107 | texImage = mat.node_tree.nodes.new("ShaderNodeTexImage")
108 | texImage.name = file
109 | texImage.image = bpy.data.images.load(path)
110 | texImage.image.use_fake_user = True
111 | texImage.location = (350, offset)
112 | offset -= 450
113 |
114 | return {"FINISHED"}
115 |
--------------------------------------------------------------------------------
/operators/maya_isolate.py:
--------------------------------------------------------------------------------
1 | # In edit mode hides unselected and enters local view
2 | # In object mode uses default blende behavior
3 |
4 | import bpy
5 |
6 |
7 | class IOPS_OT_MayaIsolate(bpy.types.Operator):
8 | """Maya-like isolate selection"""
9 |
10 | bl_idname = "iops.view3d_maya_isolate"
11 | bl_label = "IOPS Maya Isolate"
12 | bl_options = {"REGISTER", "UNDO"}
13 |
14 | @classmethod
15 | def poll(self, context):
16 | return (
17 | context.area.type == "VIEW_3D"
18 | and (context.mode == "OBJECT" or context.mode == "EDIT_MESH")
19 | and context.view_layer.objects.active.type == "MESH"
20 | and len(context.view_layer.objects.selected) != 0
21 | )
22 |
23 | def mesh_isolate(self, context):
24 | bpy.ops.mesh.hide(unselected=True)
25 | bpy.ops.view3d.localview(frame_selected=False)
26 |
27 | def object_isolate(self, context):
28 | bpy.ops.view3d.localview(frame_selected=False)
29 |
30 | def execute(self, context):
31 | if context.mode == "OBJECT":
32 | self.object_isolate(context)
33 | elif context.mode == "EDIT_MESH":
34 | self.mesh_isolate(context)
35 |
36 | return {"FINISHED"}
37 |
--------------------------------------------------------------------------------
/operators/mesh_convert_selection.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | class IOPS_OT_ToFaces(bpy.types.Operator):
5 | """Convert Vertex/Edge selection to face selection"""
6 |
7 | bl_idname = "iops.mesh_to_faces"
8 | bl_label = "Convert vertex/edge selection to face selection"
9 | bl_options = {"REGISTER", "UNDO"}
10 |
11 | @classmethod
12 | def poll(cls, context):
13 | return context.mode == "EDIT_MESH"
14 |
15 | def execute(self, context):
16 | bpy.ops.mesh.select_mode(use_expand=True, type="FACE")
17 | context.tool_settings.mesh_select_mode = (False, False, True)
18 | return {"FINISHED"}
19 |
20 |
21 | class IOPS_OT_ToEdges(bpy.types.Operator):
22 | """Convert Vertex/Face selection to edge selection"""
23 |
24 | bl_idname = "iops.mesh_to_edges"
25 | bl_label = "Convert vertex/face selection to edge selection"
26 | bl_options = {"REGISTER", "UNDO"}
27 |
28 | @classmethod
29 | def poll(cls, context):
30 | return context.mode == "EDIT_MESH"
31 |
32 | def execute(self, context):
33 | exp = False
34 | if context.tool_settings.mesh_select_mode[0]:
35 | exp = True
36 | bpy.ops.mesh.select_mode(use_expand=exp, type="EDGE")
37 | context.tool_settings.mesh_select_mode = (False, True, False)
38 | return {"FINISHED"}
39 |
40 |
41 | class IOPS_OT_ToVerts(bpy.types.Operator):
42 | """Convert Edge/Face selection to vertex selection"""
43 |
44 | bl_idname = "iops.mesh_to_verts"
45 | bl_label = "Convert edge/face selection to vertex selection"
46 | bl_options = {"REGISTER", "UNDO"}
47 |
48 | @classmethod
49 | def poll(cls, context):
50 | return context.mode == "EDIT_MESH"
51 |
52 | def execute(self, context):
53 | bpy.ops.mesh.select_mode(use_extend=True, type="VERT")
54 | context.tool_settings.mesh_select_mode = (True, False, False)
55 | return {"FINISHED"}
56 |
--------------------------------------------------------------------------------
/operators/mesh_copy_edges_angle.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import bmesh
3 | from math import degrees
4 |
5 |
6 | class IOPS_MESH_OT_CopyEdgesAngle(bpy.types.Operator):
7 | """Copy the angle between 2 selected edges"""
8 |
9 | bl_idname = "iops.mesh_copy_edges_angle"
10 | bl_label = "Copy Edges Angle"
11 |
12 | @classmethod
13 | def poll(cls, context):
14 | return (
15 | context.mode == "EDIT_MESH"
16 | and context.active_object
17 | and context.active_object.type == "MESH"
18 | )
19 |
20 | def edge_vector(self, edge):
21 | v1, v2 = edge.verts
22 | return (v2.co - v1.co).normalized()
23 |
24 | def execute(self, context):
25 | active_object = context.active_object
26 | bm = bmesh.from_edit_mesh(active_object.data)
27 | bm.edges.ensure_lookup_table()
28 |
29 | selected_edges = [e for e in bm.edges if e.select]
30 |
31 | if len(selected_edges) == 2:
32 | vec1 = self.edge_vector(selected_edges[0])
33 | vec2 = self.edge_vector(selected_edges[1])
34 |
35 | # Calculate angle in degrees
36 | angle_rad = vec1.angle(vec2)
37 | angle_deg = degrees(angle_rad)
38 |
39 | if angle_deg == 0:
40 | self.report({"WARNING"}, "Edges do not intersect")
41 | return {"FINISHED"}
42 |
43 | self.report({"INFO"}, f"Copied {angle_deg} to clipboard")
44 |
45 | bpy.context.window_manager.clipboard = str(angle_deg)
46 | bm.free()
47 | return {"FINISHED"}
48 |
49 | else:
50 | self.report({"ERROR"}, "Select only 2 edges.")
51 | return {"CANCELLED"}
52 |
--------------------------------------------------------------------------------
/operators/mesh_copy_edges_length.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import bmesh
3 |
4 |
5 | def get_unit_scale():
6 | """Get the unit scale based on the current scene units"""
7 | units = bpy.data.scenes["Scene"].unit_settings.length_unit
8 | if units == "MICROMETERS":
9 | return 0.000001
10 | elif units == "MILLIMETERS":
11 | return 0.001
12 | elif units == "CENTIMETERS":
13 | return 0.01
14 | elif units == "METERS":
15 | return 1.0
16 | elif units == "KILOMETERS":
17 | return 1000.0
18 | elif units == "ADAPTIVE":
19 | return 1.0
20 | else:
21 | return 1.0
22 |
23 |
24 | class IOPS_MESH_OT_CopyEdgesLength(bpy.types.Operator):
25 | """Copy the active edge length to the clipboard"""
26 |
27 | bl_idname = "iops.mesh_copy_edge_length"
28 | bl_label = "Copy Active Edge Length to Clipboard"
29 |
30 | @classmethod
31 | def poll(cls, context):
32 | return (
33 | context.mode == "EDIT_MESH"
34 | and context.active_object
35 | and context.active_object.type == "MESH"
36 | )
37 |
38 | def execute(self, context):
39 | active_object = context.active_object
40 | bm = bmesh.from_edit_mesh(active_object.data)
41 | bm.edges.ensure_lookup_table()
42 | selected_verts = [v for v in bm.verts if v.select]
43 | selected_edges = [e for e in bm.edges if e.select]
44 |
45 | if selected_edges:
46 | # Add the length of all selected edges and store it in the clipboard
47 | total_length = (
48 | sum(e.calc_length() for e in selected_edges) / get_unit_scale()
49 | )
50 | bpy.context.window_manager.clipboard = str(total_length)
51 | self.report({"INFO"}, "Copied the sum of edges' length to clipboard")
52 | bm.free()
53 | return {"FINISHED"}
54 | # if only 2 verts are selected, copy the distance between them
55 | elif len(selected_verts) == 2:
56 | bpy.context.window_manager.clipboard = str(
57 | (selected_verts[0].co - selected_verts[1].co).length
58 | )
59 | self.report(
60 | {"INFO"},
61 | "Copied the distance between the 2 selected verts to clipboard",
62 | )
63 | bm.free()
64 | return {"FINISHED"}
65 | else:
66 | # Invalid Selection
67 | self.report({"ERROR"}, "Invalid Selection")
68 | return {"CANCELLED"}
69 |
--------------------------------------------------------------------------------
/operators/mesh_cursor_bisect.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import mathutils
3 | import bmesh
4 | from mathutils import Vector
5 | import gpu
6 | from gpu_extras.batch import batch_for_shader
7 | # Removed heapq import as 'merge' wasn't used
8 |
9 | class IOPS_OT_Mesh_Cursor_Bisect(bpy.types.Operator):
10 | """Bisect mesh using 3D cursor position and orientation"""
11 | bl_idname = "iops.mesh_cursor_bisect"
12 | bl_label = "Cursor Bisect"
13 | bl_description = "Bisect the mesh using the 3D cursor position and orientation"
14 | bl_options = {'REGISTER', 'UNDO'}
15 |
16 | axis: bpy.props.EnumProperty(
17 | name="Bisect Axis",
18 | description="Choose the axis for bisecting",
19 | items=[
20 | ('X', "X Axis", "Bisect along the X axis"),
21 | ('Y', "Y Axis", "Bisect along the Y axis"),
22 | ('Z', "Z Axis", "Bisect along the Z axis"),
23 | ],
24 | default='Y'
25 | )
26 | merge_doubles: bpy.props.BoolProperty(
27 | name="Merge Doubles",
28 | description="Merge vertices after bisecting",
29 | default=True
30 | )
31 | merge_distance: bpy.props.FloatProperty(
32 | name="Merge Distance",
33 | description="Distance for merging vertices after bisecting",
34 | default=0.005,
35 | min=0.0,
36 | max=10.0,
37 | )
38 |
39 | _handle = None
40 | # _plane_size removed as it wasn't defined or used consistently
41 |
42 | def draw_callback(self, context):
43 | cursor = context.scene.cursor
44 | plane_co = cursor.location
45 | obj = context.object
46 | if obj is None:
47 | return
48 |
49 | cursor_mx = bpy.context.scene.cursor.matrix
50 |
51 | if self.axis == 'X':
52 | color = (1.0, 0.0, 0.0, 0.3)
53 | u = Vector((0.0, 1.0, 0.0))
54 | v = Vector((0.0, 0.0, 1.0))
55 | elif self.axis == 'Y':
56 | color = (0.0, 1.0, 0.0, 0.3)
57 | u = Vector((1.0, 0.0, 0.0))
58 | v = Vector((0.0, 0.0, 1.0))
59 | else: # self.axis == 'Z'
60 | color = (0.0, 0.0, 1.0, 0.3)
61 | u = Vector((1.0, 0.0, 0.0))
62 | v = Vector((0.0, 1.0, 0.0))
63 |
64 | u = cursor_mx.to_3x3() @ u
65 | v = cursor_mx.to_3x3() @ v
66 |
67 | size = 1.0 # Keep a fixed size for the visual aid
68 | corners = [
69 | plane_co + (u + v) * size,
70 | plane_co + (u - v) * size,
71 | plane_co + (-u - v) * size,
72 | plane_co + (-u + v) * size,
73 | ]
74 |
75 | shader = gpu.shader.from_builtin('UNIFORM_COLOR')
76 | batch = batch_for_shader(shader, 'TRI_FAN', {"pos": corners})
77 | shader.bind()
78 | shader.uniform_float("color", color)
79 | gpu.state.blend_set('ALPHA')
80 | gpu.state.depth_test_set('NONE')
81 | batch.draw(shader)
82 | gpu.state.depth_test_set('LESS')
83 | gpu.state.blend_set('NONE')
84 |
85 | def invoke(self, context, event):
86 | if context.area.type == 'VIEW_3D':
87 | self._handle = bpy.types.SpaceView3D.draw_handler_add(
88 | self.draw_callback, (context,), 'WINDOW', 'POST_VIEW'
89 | )
90 | context.window_manager.modal_handler_add(self)
91 | context.area.tag_redraw()
92 | return {'RUNNING_MODAL'}
93 | else:
94 | self.report({'WARNING'}, "View3D not found, cannot run operator")
95 | return {'CANCELLED'}
96 |
97 | def modal(self, context, event):
98 | if event.type in {'MIDDLEMOUSE'}:
99 | return {'PASS_THROUGH'}
100 | elif event.type in {'ESC', 'RIGHTMOUSE'}:
101 | self.cancel(context)
102 | return {'CANCELLED'}
103 | elif event.type == 'LEFTMOUSE':
104 | self.execute(context)
105 | self.cancel(context)
106 | return {'FINISHED'}
107 | elif event.type == 'WHEELUPMOUSE':
108 | self.axis = {'X': 'Y', 'Y': 'Z', 'Z': 'X'}[self.axis]
109 | context.area.tag_redraw()
110 | return {'RUNNING_MODAL'} # Stay modal
111 | elif event.type == 'WHEELDOWNMOUSE':
112 | self.axis = {'X': 'Z', 'Z': 'Y', 'Y': 'X'}[self.axis]
113 | context.area.tag_redraw()
114 | return {'RUNNING_MODAL'} # Stay modal
115 | # Removed arrow key handling for plane size as it wasn't fully implemented
116 |
117 | # Redraw requested for axis changes or potentially other interactions
118 | if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
119 | context.area.tag_redraw()
120 |
121 | return {'RUNNING_MODAL'}
122 |
123 |
124 | def execute(self, context):
125 | obj = context.object
126 | scene = context.scene
127 | cursor = scene.cursor
128 |
129 | if not obj:
130 | self.report({'ERROR'}, "No active object selected.")
131 | return {'CANCELLED'}
132 | if obj.type != 'MESH':
133 | self.report({'ERROR'}, "Active object is not a Mesh.")
134 | return {'CANCELLED'}
135 | if obj.mode != 'EDIT':
136 | self.report({'ERROR'}, "Not in Edit Mode. Please switch to Edit Mode.")
137 | return {'CANCELLED'}
138 |
139 | obj_mat_world = obj.matrix_world
140 | obj_mat_world_inv = obj_mat_world.inverted()
141 | cursor_mat_world = cursor.matrix
142 |
143 | plane_co_local = obj_mat_world_inv @ cursor.location
144 |
145 | if self.axis == 'X':
146 | base_normal = mathutils.Vector((1, 0, 0))
147 | elif self.axis == 'Y':
148 | base_normal = mathutils.Vector((0, 1, 0))
149 | else: # self.axis == 'Z'
150 | base_normal = mathutils.Vector((0, 0, 1))
151 |
152 | normal_world = cursor_mat_world.to_3x3() @ base_normal
153 | normal_world.normalize()
154 |
155 | plane_no_local = obj_mat_world_inv.to_3x3() @ normal_world
156 | plane_no_local.normalize()
157 |
158 | bm = bmesh.from_edit_mesh(obj.data)
159 |
160 | selected_faces = [f for f in bm.faces if f.select]
161 | if not selected_faces:
162 | # If no faces selected, operate on the whole mesh implicitly
163 | # by not restricting the 'geom' input (or providing all geometry)
164 | # For bisect_plane, providing None or all geom works
165 | geom = bm.verts[:] + bm.edges[:] + bm.faces[:] # Operate on everything
166 | else:
167 | # Operate only on selected faces and their constituent edges/verts
168 | geom_faces = set(selected_faces)
169 | geom_edges = set(e for f in geom_faces for e in f.edges)
170 | geom_verts = set(v for f in geom_faces for v in f.verts)
171 | geom = list(geom_verts) + list(geom_edges) + list(geom_faces)
172 |
173 |
174 | try:
175 | result = bmesh.ops.bisect_plane(
176 | bm,
177 | geom=geom,
178 | plane_co=plane_co_local,
179 | plane_no=plane_no_local,
180 | # clear_inner=False, # Default
181 | # clear_outer=False, # Default
182 | # use_fill=False # Default
183 | )
184 |
185 | if self.merge_doubles:
186 | bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.merge_distance)
187 | else:
188 | if selected_faces:
189 | for f in selected_faces:
190 | if f.is_valid:
191 | f.select_set(False)
192 |
193 | if result and 'geom_cut' in result:
194 | for element in result['geom_cut']:
195 | if element.is_valid:
196 | element.select_set(True)
197 | bm.select_flush(True)
198 |
199 | bmesh.update_edit_mesh(obj.data)
200 |
201 | except Exception as e:
202 | self.report({'ERROR'}, f"BMesh bisect failed: {e}")
203 | # Avoid freeing BMesh here as 'from_edit_mesh' handles it
204 | return {'CANCELLED'}
205 |
206 | return {'FINISHED'}
207 |
208 | def cancel(self, context):
209 | if self._handle is not None:
210 | bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
211 | self._handle = None
212 | # Ensure redraw happens on cancel to remove the visual aid
213 | if context.area:
214 | context.area.tag_redraw()
215 |
--------------------------------------------------------------------------------
/operators/mesh_quick_snap.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import bmesh
3 | from mathutils import Vector
4 | from bpy.props import BoolProperty
5 |
6 |
7 | class IOPS_OT_Mesh_QuickSnap(bpy.types.Operator):
8 | """Quick Snap point to point"""
9 |
10 | bl_idname = "iops.mesh_quick_snap"
11 | bl_label = "IOPS Quick Snap"
12 | bl_options = {"REGISTER", "UNDO"}
13 |
14 | quick_snap_surface: BoolProperty(
15 | name="Surface snap", description="ON/Off", default=False
16 | )
17 | # @classmethod
18 | # def poll(cls, context):
19 | # return (and context.area.type == "VIEW_3D")
20 |
21 | def execute(self, context):
22 | if context.mode == "EDIT_MESH":
23 | scene = context.scene
24 |
25 | edit_obj = context.active_object
26 | me = edit_obj.data
27 | target_points = []
28 | # GET INDEXES
29 | bm = bmesh.from_edit_mesh(me)
30 | selected_verts_index = []
31 | for v in bm.verts:
32 | if v.select:
33 | selected_verts_index.append(v.index)
34 | bpy.ops.object.editmode_toggle()
35 | bm.free()
36 | bpy.ops.object.editmode_toggle()
37 |
38 | # GET SCENE OBJECTS
39 | mesh_objects = [
40 | o
41 | for o in scene.objects
42 | if o.type == "MESH"
43 | and o.data.polygons[:] != []
44 | and o.visible_get()
45 | and o.modifiers[:] == []
46 | ]
47 | bm = bmesh.new()
48 | for ob in mesh_objects:
49 | if ob == edit_obj:
50 | continue
51 | ob_mw_i = ob.matrix_world.inverted()
52 |
53 | bm.from_mesh(me)
54 | bm.verts.ensure_lookup_table()
55 | for ind in selected_verts_index:
56 | vert = bm.verts[ind]
57 | vert_co = edit_obj.matrix_world @ vert.co
58 | local_pos = ob_mw_i @ vert_co
59 | (hit, loc, norm, face_index) = ob.closest_point_on_mesh(local_pos)
60 | if hit:
61 | bm.verts.ensure_lookup_table()
62 | bm.faces.ensure_lookup_table()
63 | v_dists = {}
64 | if self.quick_snap_surface:
65 | target_co = ob.matrix_world @ loc
66 | v_dist = (target_co - vert_co).length
67 | min_co = target_co
68 | min_len = v_dist
69 | else:
70 | for v in ob.data.polygons[face_index].vertices:
71 | target_co = ob.matrix_world @ ob.data.vertices[v].co
72 | v_dist = (target_co - vert_co).length
73 | v_dists[v] = {}
74 | v_dists[v]["co"] = (*target_co,)
75 | v_dists[v]["len"] = v_dist
76 |
77 | lens = [v_dists[idx]["len"] for idx in v_dists]
78 | for k in v_dists.values():
79 | if k["len"] == min(lens):
80 | min_co = Vector((k["co"]))
81 | min_len = k["len"]
82 |
83 | if target_points:
84 | if len(target_points) != len(selected_verts_index):
85 | target_points.append([ind, min_co, min_len])
86 | else:
87 | for p in target_points:
88 | if p[0] == ind and p[2] >= min_len:
89 | p[1] = min_co
90 | p[2] = min_len
91 | else:
92 | target_points.append([ind, min_co, min_len])
93 | bm.clear()
94 |
95 | bm = bmesh.from_edit_mesh(me)
96 | bm.verts.ensure_lookup_table()
97 | for p in target_points:
98 | bm.verts[p[0]].co = edit_obj.matrix_world.inverted() @ p[1]
99 | bmesh.update_edit_mesh(me)
100 |
101 | self.report({"INFO"}, "POINTS ARE SNAPPED!!!")
102 | else:
103 | self.report({"WARNING"}, "ENTER TO MESH EDIT MODE!!!")
104 | return {"FINISHED"}
105 |
--------------------------------------------------------------------------------
/operators/mesh_to_grid.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import bmesh
3 | from bpy.props import FloatProperty
4 |
5 |
6 | class IOPS_OT_mesh_to_grid(bpy.types.Operator):
7 | """Gridify vertex position"""
8 |
9 | bl_idname = "iops.mesh_to_grid"
10 | bl_label = "IOPS mesh_to_grid"
11 | bl_options = {"REGISTER", "UNDO"}
12 |
13 | base: FloatProperty(
14 | name="Base",
15 | description="Nearest grid number in scene units (0.01 = 1cm, 10 = 10m)",
16 | default=0.01,
17 | soft_min=0.01,
18 | soft_max=10,
19 | )
20 |
21 | @classmethod
22 | def poll(cls, context):
23 | return context.mode == "EDIT_MESH" and context.area.type == "VIEW_3D"
24 |
25 | def round_to_base(self, coord, base):
26 | return base * round(coord / base)
27 |
28 | def execute(self, context):
29 | dg = bpy.context.evaluated_depsgraph_get()
30 | ob = context.view_layer.objects.active
31 | bm = bmesh.from_edit_mesh(ob.data)
32 |
33 | for v in bm.verts:
34 | pos_x = self.round_to_base(v.co[0], self.base)
35 | pos_y = self.round_to_base(v.co[1], self.base)
36 | pos_z = self.round_to_base(v.co[2], self.base)
37 |
38 | v.co = (pos_x, pos_y, pos_z)
39 |
40 | bmesh.update_edit_mesh(ob.data)
41 | dg.update()
42 | self.report({"INFO"}, "Vertices snapped to grid")
43 | return {"FINISHED"}
44 |
--------------------------------------------------------------------------------
/operators/mesh_uv_channel_hop.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import bmesh
3 |
4 | from bpy.props import (
5 | BoolProperty,
6 | )
7 |
8 |
9 | class IOPS_OT_Mesh_UV_Channel_Hop(bpy.types.Operator):
10 | """Cyclic switching uv channels and uv seams on active object"""
11 |
12 | bl_idname = "iops.mesh_uv_channel_hop"
13 | bl_label = "Object UV Channel Hop"
14 | bl_options = {"REGISTER", "UNDO"}
15 |
16 | mark_seam: BoolProperty(
17 | name="Mark Seams", description="Mark seams by UV Islands", default=True
18 | )
19 | hop_previous: BoolProperty(
20 | name="Hop to Previous", description="Hop to previous UV Channel", default=False
21 | )
22 | set_render: BoolProperty(
23 | name="Set Render Channel", description="Set Render Channel", default=True
24 | )
25 |
26 | @classmethod
27 | def poll(cls, context):
28 | return (
29 | context.area.type == "VIEW_3D"
30 | and context.active_object
31 | and context.active_object.type == "MESH"
32 | and context.active_object.mode == "EDIT"
33 | )
34 |
35 | def execute(self, context):
36 | # Switch UV Channel by modulo of uv_layers length and set render channel
37 | ob = context.active_object
38 | me = ob.data
39 | # save curentrly selected faces, deselect all faces, select all faces
40 | bm = bmesh.from_edit_mesh(me)
41 | # list of selected faces
42 | selected_faces = [f for f in bm.faces if f.select]
43 | # Deselect all and select all faces
44 | bpy.ops.mesh.select_all(action="DESELECT")
45 | bpy.ops.mesh.select_all(action="SELECT")
46 | # switch uv channel
47 | if self.hop_previous:
48 | ob.data.uv_layers.active_index = (ob.data.uv_layers.active_index - 1) % len(
49 | ob.data.uv_layers
50 | )
51 | else:
52 | ob.data.uv_layers.active_index = (ob.data.uv_layers.active_index + 1) % len(
53 | ob.data.uv_layers
54 | )
55 | # clear seams and mark seams by UV Islands
56 | if self.mark_seam:
57 | bpy.ops.mesh.mark_seam(clear=True)
58 | bpy.ops.uv.select_all(action="SELECT")
59 | bpy.ops.uv.seams_from_islands(mark_seams=True)
60 | # set active render channel
61 | if self.set_render:
62 | ob.data.uv_layers[ob.data.uv_layers.active_index].active_render = True
63 | # Deselect all and select previously selected faces
64 | bpy.ops.mesh.select_all(action="DESELECT")
65 | # Restore selection from selected_faces list
66 | if selected_faces:
67 | for f in selected_faces:
68 | f.select = True
69 | bmesh.update_edit_mesh(me)
70 | # Get active render channel
71 | active_render = ""
72 | for uv_layer in ob.data.uv_layers:
73 | if uv_layer.active_render:
74 | active_render = uv_layer.name
75 | # Print report
76 | self.report(
77 | {"INFO"},
78 | f"UV Active Channel: {ob.data.uv_layers.active.name}, UV Active Render: {active_render}",
79 | )
80 | return {"FINISHED"}
81 |
--------------------------------------------------------------------------------
/operators/modes.py:
--------------------------------------------------------------------------------
1 | from .iops import IOPS_OT_Main
2 |
3 |
4 | class IOPS_OT_F1(IOPS_OT_Main):
5 | bl_idname = "iops.function_f1"
6 | bl_label = "IOPS OPERATOR F1"
7 | operator = "F1"
8 |
9 |
10 | class IOPS_OT_F2(IOPS_OT_Main):
11 | bl_idname = "iops.function_f2"
12 | bl_label = "IOPS OPERATOR F2"
13 | operator = "F2"
14 |
15 |
16 | class IOPS_OT_F3(IOPS_OT_Main):
17 | bl_idname = "iops.function_f3"
18 | bl_label = "IOPS OPERATOR F3"
19 | operator = "F3"
20 |
21 |
22 | class IOPS_OT_F4(IOPS_OT_Main):
23 | bl_idname = "iops.function_f4"
24 | bl_label = "IOPS OPERATOR F4"
25 | operator = "F4"
26 |
27 |
28 | class IOPS_OT_F5(IOPS_OT_Main):
29 | bl_idname = "iops.function_f5"
30 | bl_label = "IOPS OPERATOR F5"
31 | operator = "F5"
32 |
33 |
34 | class IOPS_OT_ESC(IOPS_OT_Main):
35 | bl_idname = "iops.function_esc"
36 | bl_label = "IOPS OPERATOR ESC"
37 | operator = "ESC"
38 |
--------------------------------------------------------------------------------
/operators/mouseover_fill_select.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | class IOPS_MouseoverFillSelect(bpy.types.Operator):
5 | """Fill select faces at mouseover"""
6 |
7 | bl_idname = "iops.mesh_mouseover_fill_select"
8 | bl_label = "IOPS Mouseover Fill Select"
9 | bl_options = {"REGISTER", "UNDO"}
10 |
11 | @classmethod
12 | def poll(cls, context):
13 | return (
14 | context.object is not None
15 | and context.object.type == "MESH"
16 | and context.object.data.is_editmode
17 | )
18 |
19 | def invoke(self, context, event):
20 | loc = event.mouse_region_x, event.mouse_region_y
21 | bpy.ops.mesh.hide(unselected=False)
22 | bpy.ops.view3d.select(extend=True, location=loc)
23 | bpy.ops.mesh.select_linked(delimit={"NORMAL"})
24 | bpy.ops.mesh.reveal(select=True)
25 | return {"FINISHED"}
26 |
--------------------------------------------------------------------------------
/operators/object_auto_smooth.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | from math import radians
4 |
5 | # Check box to put the modifier at the top of the stack or bottom
6 |
7 | # def move_modifier_to_top(obj, mod):
8 | # if obj.modifiers:
9 | # while obj.modifiers[0] != mod:
10 | # bpy.ops.object.modifier_move_up(modifier=mod.name)
11 |
12 |
13 | class IOPS_OT_AutoSmooth(bpy.types.Operator):
14 | bl_idname = "iops.object_auto_smooth"
15 | bl_description = "Add Auto Smooth modifier to selected objects"
16 | bl_label = "Add Auto Smooth Modifier"
17 | bl_options = {"REGISTER", "UNDO"}
18 |
19 | angle: bpy.props.FloatProperty(
20 | name="Smooth Angle",
21 | description="Smooth Angle",
22 | default=30.0,
23 | min=0.0,
24 | max=180.0,
25 | )
26 |
27 | ignore_sharp: bpy.props.BoolProperty(
28 | name="Ignore Sharp Edges",
29 | description="Ignore Sharp Edges",
30 | default=False,
31 | )
32 |
33 | stack_top: bpy.props.BoolProperty(
34 | name="Top of Stack",
35 | description="Add modifier to top of stack",
36 | default=True,
37 | )
38 |
39 | @classmethod
40 | def poll(self, context):
41 | # True if any of the selected objects are meshes
42 | return any(obj.type == "MESH" for obj in bpy.context.selected_objects)
43 |
44 | def invoke(self, context, event):
45 | # check if bpy.data.node_groups["Smooth by Angle"] exists, if not import it
46 | if "Smooth by Angle" not in bpy.data.node_groups.keys():
47 | res_path = bpy.utils.resource_path("LOCAL")
48 | path = os.path.join(
49 | res_path, "datafiles\\assets\\geometry_nodes\\smooth_by_angle.blend"
50 | )
51 |
52 | with bpy.data.libraries.load(path, link=True) as (data_from, data_to):
53 | data_to.node_groups = data_from.node_groups
54 | # print(f"Loaded {path}")
55 | return self.execute(context)
56 |
57 | def execute(self, context):
58 | if context.object.mode == 'OBJECT':
59 | bpy.ops.object.shade_smooth()
60 | else:
61 | bpy.ops.object.mode_set(mode='OBJECT')
62 | bpy.ops.object.shade_smooth()
63 | bpy.ops.object.mode_set(mode='EDIT')
64 | count = 1
65 | meshes = [obj for obj in bpy.context.selected_objects if obj.type == "MESH"]
66 | for mesh in meshes:
67 | auto_smooth_exists = False
68 | # Only change parameters if existing Auto Smooth has node_groups["Smooth by Angle"]
69 | for mod in mesh.modifiers:
70 | if (
71 | "Auto Smooth" in mod.name
72 | and mod.type == "NODES"
73 | and getattr(mod.node_group, "name", None) == "Smooth by Angle"
74 | ):
75 | print(
76 | f"Auto Smooth modifier exists on {mesh.name}. Changing parameters."
77 | )
78 | mod["Input_1"] = radians(self.angle)
79 | mod["Socket_1"] = self.ignore_sharp
80 | # redraw ui
81 | areas = [
82 | area
83 | for area in bpy.context.screen.areas
84 | if area.type == "PROPERTIES"
85 | ]
86 | if areas:
87 | for area in areas:
88 | with bpy.context.temp_override():
89 | area.tag_redraw()
90 | auto_smooth_exists = True
91 | break
92 |
93 | if auto_smooth_exists:
94 | continue
95 |
96 | if getattr(mesh, "auto_smooth_modifier", None) == "Auto Smooth":
97 | mesh.auto_smooth_modifier = "Auto Smooth"
98 | print(
99 | f"Adding Auto Smooth modifier to {mesh.name}, {count} of {len(bpy.context.selected_objects)}"
100 | )
101 |
102 | count += 1
103 |
104 | with bpy.context.temp_override(object=mesh, active_object=mesh, selected_objects=[mesh]):
105 | # Shade Smooth
106 | # bpy.ops.object.shade_smooth()
107 | # Delete existing Auto Smooth modifier
108 | for mod in mesh.modifiers:
109 | if (
110 | "Auto Smooth" in mod.name
111 | and mod.type == "NODES"
112 | and getattr(mod.node_group, "name", None) == "Auto Smooth"
113 | ):
114 | try:
115 | bpy.ops.object.modifier_remove(modifier=mod.name)
116 | except Exception as e:
117 | print(
118 | f"Could not remove Auto Smooth modifier from {mesh.name} — {e}"
119 | )
120 | break
121 |
122 | # Add Smooth by Angle modifier from Essentials library
123 | try:
124 | if "Auto Smooth" not in [mod.name for mod in mesh.modifiers]:
125 | bpy.ops.object.modifier_add_node_group(
126 | asset_library_type="ESSENTIALS",
127 | asset_library_identifier="",
128 | relative_asset_identifier="geometry_nodes/smooth_by_angle.blend/NodeTree/Smooth by Angle",
129 | )
130 | for _mod in mesh.modifiers:
131 | if _mod.type == "NODES" and "Smooth by Angle" in _mod.name:
132 | _mod.name = "Auto Smooth"
133 | _mod["Input_1"] = radians(self.angle)
134 | _mod["Socket_1"] = self.ignore_sharp
135 | _mod.name = "Auto Smooth"
136 | mod = _mod
137 |
138 | # if mod.type == "NODES":
139 | # mod.node_group = bpy.data.node_groups["Smooth by Angle"]
140 | # mod["Input_1"] = radians(self.angle)
141 | # mod["Socket_1"] = self.ignore_sharp
142 |
143 | else:
144 | mod = mesh.modifiers["Auto Smooth"]
145 | except Exception as e:
146 | print(f"Could not add Auto Smooth modifier to {mesh.name} — {e}")
147 | continue
148 | mod.show_viewport = False
149 | mod.show_viewport = True
150 | if self.stack_top:
151 | bpy.ops.object.modifier_move_to_index(modifier=mod.name, index=0)
152 |
153 | else:
154 | bpy.ops.object.modifier_move_to_index(
155 | modifier=mod.name, index=len(mesh.modifiers) - 1
156 | )
157 |
158 | return {"FINISHED"}
159 |
160 |
161 | class IOPS_OT_ClearCustomNormals(bpy.types.Operator):
162 | bl_idname = "iops.object_clear_normals"
163 | bl_description = "Remove custom normals from selected objects"
164 | bl_label = "Clear Custom Normals"
165 | bl_options = {"REGISTER", "UNDO"}
166 |
167 | @classmethod
168 | def poll(self, context):
169 | # True if any of the selected objects are meshes and have custom normals (obj.data.has_custom_normals)
170 | return any(
171 | obj.type == "MESH" and getattr(obj.data, "has_custom_normals", False)
172 | for obj in bpy.context.selected_objects
173 | )
174 |
175 | def execute(self, context):
176 | count = 1
177 | for obj in bpy.context.selected_objects:
178 | print(
179 | f"Clearing custom normals from {obj.name}, {count} of {len(bpy.context.selected_objects)}"
180 | )
181 | count += 1
182 | with bpy.context.temp_override(object=obj):
183 | if obj.type == "MESH":
184 | bpy.ops.mesh.customdata_custom_splitnormals_clear()
185 | return {"FINISHED"}
186 |
--------------------------------------------------------------------------------
/operators/object_match_transform_active.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | class IOPS_OT_MatchTransformActive(bpy.types.Operator):
5 | """Match dimensions of selected object to active"""
6 |
7 | bl_idname = "iops.object_match_transform_active"
8 | bl_label = "Match dimensions"
9 | bl_options = {"REGISTER", "UNDO"}
10 |
11 | @classmethod
12 | def poll(self, context):
13 | return (
14 | context.area.type == "VIEW_3D"
15 | and context.mode == "OBJECT"
16 | and len(context.view_layer.objects.selected) != 0
17 | and context.view_layer.objects.active.type == "MESH"
18 | )
19 |
20 | def execute(self, context):
21 | selection = context.view_layer.objects.selected
22 | active = context.view_layer.objects.active
23 |
24 | for ob in selection:
25 | ob.dimensions = active.dimensions
26 |
27 | self.report({"INFO"}, "Dimensions matched")
28 |
29 | return {"FINISHED"}
30 |
--------------------------------------------------------------------------------
/operators/object_name_from_active.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import re
3 | from bpy.props import (
4 | IntProperty,
5 | StringProperty,
6 | BoolProperty,
7 | )
8 | from mathutils import Vector
9 |
10 |
11 | def distance_vec(point1: Vector, point2: Vector):
12 | """Calculate distance between two points."""
13 | return (point2 - point1).length
14 |
15 |
16 | class IOPS_OT_Object_Name_From_Active(bpy.types.Operator):
17 | """Rename Object as Active ObjectName"""
18 |
19 | bl_idname = "iops.object_name_from_active"
20 | bl_label = "IOPS Object Name From Active"
21 | bl_options = {"REGISTER", "UNDO"}
22 |
23 | new_name: StringProperty(
24 | name="New Name",
25 | default="",
26 | )
27 |
28 | active_name: StringProperty(
29 | name="Active Name",
30 | default="",
31 | )
32 |
33 | pattern: StringProperty(
34 | name="Pattern",
35 | description="""Naming Syntaxis:
36 | [N] - Name
37 | [C] - Counter
38 | [T] - Object Type
39 | [COL] - Collection Name
40 | """,
41 | default="[N]_[C]",
42 | )
43 |
44 | use_distance: BoolProperty(
45 | name="By Distance",
46 | description="Rename Selected Objects Based on Distance to Active Object",
47 | default=True,
48 | )
49 |
50 | counter_digits: IntProperty(
51 | name="Counter Digits",
52 | description="Number Of Digits For Counter",
53 | default=2,
54 | min=2,
55 | max=10,
56 | )
57 |
58 | counter_shift: BoolProperty(
59 | name="+1",
60 | description="+1 shift for counter, useful when we need to rename active object too",
61 | default=True,
62 | )
63 |
64 | rename_active: BoolProperty(
65 | name="Rename Active", description="Rename active also", default=True
66 | )
67 |
68 | rename_mesh_data: BoolProperty(
69 | name="Rename Mesh Data", description="Rename Mesh Data", default=True
70 | )
71 |
72 | trim_prefix: IntProperty(
73 | name="Prefix",
74 | description="Number Of Digits for Prefix trim",
75 | default=0,
76 | min=0,
77 | max=100,
78 | )
79 |
80 | trim_suffix: IntProperty(
81 | name="Suffix",
82 | description="Number Of Digits for Suffix trim",
83 | default=0,
84 | min=0,
85 | max=100,
86 | )
87 | use_trim: BoolProperty(
88 | name="Trim", description="Trim Name Prefix/Suffix", default=False
89 | )
90 | rename_linked: BoolProperty(
91 | name="Rename Linked",
92 | description="Rename Linked Objects",
93 | default=False,
94 | )
95 |
96 | def invoke(self, context, event):
97 | self.active_name = context.view_layer.objects.active.name
98 | self.new_name = self.active_name
99 | return self.execute(context)
100 |
101 | def execute(self, context):
102 | if self.pattern:
103 | if self.active_name != context.view_layer.objects.active.name:
104 | context.view_layer.objects.active.name = self.active_name
105 | # Trim string
106 | if self.use_trim:
107 | name = self.active_name
108 | if self.trim_suffix == 0:
109 | self.new_name = name[(self.trim_prefix) :]
110 | else:
111 | self.new_name = name[(self.trim_prefix) : -(self.trim_suffix)]
112 | else:
113 | self.trim_suffix = self.trim_prefix = 0
114 |
115 | digit = "{0:0>" + str(self.counter_digits) + "}"
116 | # Combine objects
117 | active = bpy.context.view_layer.objects.active
118 | Objects = bpy.context.selected_objects
119 | if active:
120 | active_collection = active.users_collection[0]
121 | else:
122 | active_collection = ""
123 | to_rename = []
124 | if self.use_distance:
125 | Objects.sort(key=lambda obj: (obj.location - active.location).length)
126 | # Check active
127 | if self.rename_active:
128 | to_rename = [ob.name for ob in Objects]
129 | else:
130 | to_rename = [ob.name for ob in Objects if ob is not active]
131 | # counter
132 | counter = 0
133 | if self.counter_shift:
134 | counter = 1
135 |
136 | if self.rename_linked:
137 | if active.children_recursive:
138 | to_rename.extend([child.name for child in active.children_recursive])
139 |
140 |
141 | for name in to_rename:
142 | o = bpy.data.objects[name]
143 | pattern = re.split(r"(\[\w+\])", self.pattern)
144 | # i - index, p - pattern
145 | for i, p in enumerate(pattern):
146 | if p == "[N]":
147 | pattern[i] = self.new_name
148 | if p == "[C]":
149 | pattern[i] = digit.format(counter)
150 | if p == "[T]":
151 | pattern[i] = o.type.lower()
152 | if p == "[COL]":
153 | pattern[i] = active_collection.name
154 | o.name = "".join(pattern)
155 | # Rename object mesh data
156 | if self.rename_mesh_data:
157 | if o.type == "MESH":
158 | o.data.name = o.name
159 | counter += 1
160 | else:
161 | self.report({"ERROR"}, "Please fill the pattern field")
162 | return {"FINISHED"}
163 |
164 | def draw(self, context):
165 | layout = self.layout
166 | col_active_name = layout.column(align=True)
167 | col_active_name.enabled = False
168 | col_active_name.prop(self, "active_name", text="Old Name")
169 |
170 | col = layout.column(align=True)
171 | col.prop(self, "new_name", text="New Name")
172 | col.separator()
173 | col = layout.column(align=True)
174 | # Trim
175 | row = col.row(align=True)
176 | sub = row.row(align=True)
177 | sub.enabled = self.use_trim
178 | sub.prop(self, "trim_prefix")
179 | row.prop(self, "use_trim", toggle=True)
180 | sub = row.row(align=True)
181 | sub.enabled = self.use_trim
182 | sub.prop(self, "trim_suffix")
183 | # Pattern
184 | col = layout.column(align=True)
185 | col.separator()
186 | col.prop(self, "pattern")
187 | col.separator()
188 | row = col.row(align=True)
189 | row.label(text="Counter Digits:")
190 | row.alignment = "LEFT"
191 | row.prop(self, "counter_digits", text=" ")
192 | row.separator(factor=1.0)
193 | row.prop(self, "counter_shift")
194 | col = layout.column(align=True)
195 | col.label(text="Rename:")
196 |
197 | col.prop(self, "rename_active", text="Active Object")
198 | col.prop(self, "use_distance", text="By Distance")
199 | col.prop(self, "rename_mesh_data", text="Object's MeshData")
200 | col.prop(self, "rename_linked", text="Linked Objects")
201 |
--------------------------------------------------------------------------------
/operators/object_normalize.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import math
3 | from mathutils import Vector
4 | from bpy.props import BoolProperty, IntProperty
5 |
6 |
7 | class IOPS_OT_object_normalize(bpy.types.Operator):
8 | """Normalize location,Rotation,Scale,Dimensions values"""
9 |
10 | bl_idname = "iops.object_normalize"
11 | bl_label = "IOPS object normalize"
12 | bl_options = {"REGISTER", "UNDO"}
13 |
14 | precision: IntProperty(
15 | name="Precision",
16 | description="Digits after point",
17 | default=2,
18 | soft_min=0,
19 | soft_max=10,
20 | )
21 | location: BoolProperty(
22 | name="Trim location", description="Trim location values", default=True
23 | )
24 | rotation: BoolProperty(
25 | name="Trim rotation", description="Trim rotation values", default=True
26 | )
27 |
28 | dimensions: BoolProperty(
29 | name="Trim dimentsion", description="Trim dimentsion values", default=True
30 | )
31 |
32 | @classmethod
33 | def poll(self, context):
34 | return (
35 | context.area.type == "VIEW_3D"
36 | and context.mode == "OBJECT"
37 | and len(context.view_layer.objects.selected) != 0
38 | and (
39 | context.view_layer.objects.active.type == "MESH"
40 | or context.view_layer.objects.active.type == "EMPTY"
41 | )
42 | )
43 |
44 | def execute(self, context):
45 | selection = context.view_layer.objects.selected
46 | dg = bpy.context.evaluated_depsgraph_get()
47 | for ob in selection:
48 | if self.location:
49 | pos_x = round(ob.location.x, self.precision)
50 | pos_y = round(ob.location.y, self.precision)
51 | pos_z = round(ob.location.z, self.precision)
52 | ob.location.x = pos_x
53 | ob.location.y = pos_y
54 | ob.location.z = pos_z
55 |
56 | if self.rotation:
57 | rot_x = round(math.degrees(ob.rotation_euler.x), self.precision)
58 | rot_y = round(math.degrees(ob.rotation_euler.y), self.precision)
59 | rot_z = round(math.degrees(ob.rotation_euler.z), self.precision)
60 | ob.rotation_euler.x = math.radians(rot_x)
61 | ob.rotation_euler.y = math.radians(rot_y)
62 | ob.rotation_euler.z = math.radians(rot_z)
63 |
64 | if self.dimensions:
65 | dim_x = round(ob.dimensions.x, self.precision)
66 | dim_y = round(ob.dimensions.y, self.precision)
67 | dim_z = round(ob.dimensions.z, self.precision)
68 | ob.dimensions = Vector((dim_x, dim_y, dim_z))
69 | dg.update()
70 | self.report({"INFO"}, "Object dimensions normalized")
71 | return {"FINISHED"}
72 |
--------------------------------------------------------------------------------
/operators/object_replace.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import random
3 | from bpy.props import (
4 | BoolProperty,
5 | )
6 | from ..utils.functions import get_active_and_selected
7 |
8 |
9 | def get_random_obj_from_list(list):
10 | if len(list) != 0:
11 | obj = random.choice(list)
12 | return obj
13 |
14 |
15 | class IOPS_OT_Object_Replace(bpy.types.Operator):
16 | """Replace objects with active"""
17 |
18 | bl_idname = "iops.object_replace"
19 | bl_label = "IOPS Object Replace"
20 | bl_options = {"REGISTER", "UNDO"}
21 |
22 | # use_active_collection: BoolProperty(
23 | # name="Active Collection",
24 | # description="Use Active Collection",
25 | # default=False
26 | # )
27 | replace: BoolProperty(
28 | name="Add/Replace",
29 | description="Enabled = Replace, Disabled = Add",
30 | default=True,
31 | )
32 | select_replaced: BoolProperty(
33 | name="Select Replaced",
34 | description="Enabled = Select Replaced Objects, Disabled = Keep Selection",
35 | default=True,
36 | )
37 |
38 | keep_rotation: BoolProperty(
39 | name="Keep Rotation",
40 | description="Enabled = Use Active Object Rotation, Disabled = Use Selected Object Rotation",
41 | default=False,
42 | )
43 |
44 | keep_scale: BoolProperty(
45 | name="Keep Scale",
46 | description="Enabled = Use Active Object Scale, Disabled = Use Selected Object Scale",
47 | default=False,
48 | )
49 |
50 | keep_active_object_collection: BoolProperty(
51 | name="Keep Active Object Collection",
52 | description="Enabled = Use Active Object Collection, Disabled = Object Replace Collection",
53 | default=True,
54 | )
55 |
56 | keep_object_collection: BoolProperty(
57 | name="Keep Object Collection",
58 | description="Enabled = Use Selected Object Collection, Disabled = Object Replace Collection. Keep in mind that this option will override the Keep Active Object Collection option.",
59 | default=True,
60 | )
61 |
62 | def execute(self, context):
63 | active, objects = get_active_and_selected()
64 | if active and objects:
65 |
66 | if self.keep_active_object_collection:
67 | collection = active.users_collection[0]
68 | else:
69 | collection = bpy.data.collections.new("Object Replace")
70 | bpy.context.scene.collection.children.link(collection)
71 |
72 | new_objects = []
73 | for ob in objects:
74 | if self.keep_object_collection:
75 | collection = ob.users_collection[0]
76 | new_ob = active.copy()
77 | if active.type == "MESH":
78 | new_ob.data = active.data.copy()
79 | # position
80 | new_ob.location = ob.location
81 | # scale
82 | if self.keep_scale:
83 | new_ob.scale = active.scale
84 | else:
85 | new_ob.scale = ob.scale
86 | # rotation
87 | if self.keep_rotation:
88 | new_ob.rotation_euler = active.rotation_euler
89 | else:
90 | new_ob.rotation_euler = ob.rotation_euler
91 |
92 | collection.objects.link(new_ob)
93 | new_ob.select_set(False)
94 | new_objects.append(new_ob)
95 |
96 | if self.select_replaced:
97 | active.select_set(False)
98 | for ob in objects:
99 | ob.select_set(False)
100 | for ob in new_objects:
101 | ob.select_set(True)
102 | bpy.context.view_layer.objects.active = new_objects[-1]
103 |
104 | if self.replace:
105 | for ob in objects:
106 | bpy.data.objects.remove(
107 | ob, do_unlink=True, do_id_user=True, do_ui_user=True
108 | )
109 | self.report({"INFO"}, "Objects Were Replaced")
110 | else:
111 | self.report({"ERROR"}, "Please Select Objects")
112 | return {"FINISHED"}
113 |
114 | def draw(self, context):
115 | layout = self.layout
116 | col = layout.column(align=True)
117 | col.prop(self, "keep_active_object_collection")
118 | col.prop(self, "keep_object_collection")
119 | # col.prop(self, "use_active_collection")
120 | col.prop(self, "replace")
121 | col.prop(self, "select_replaced")
122 | col.separator()
123 | col.prop(self, "keep_rotation")
124 | col.prop(self, "keep_scale")
125 |
--------------------------------------------------------------------------------
/operators/object_uvmaps_add_remove.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 |
5 | def tag_redraw(context, space_type="VIEW_3D", region_type="WINDOW"):
6 | for window in context.window_manager.windows:
7 | for area in window.screen.areas:
8 | if area.spaces[0].type == space_type:
9 | for region in area.regions:
10 | if region.type == region_type:
11 | region.tag_redraw()
12 |
13 | def uvmap_add(obj):
14 | obj_uvmaps = obj.data.uv_layers
15 | ch_num_text = "ch" + str(len(obj_uvmaps) + 1)
16 | obj_uvmaps.new(name=ch_num_text, do_init=True)
17 |
18 |
19 | def uvmap_clean_by_name(obj, name):
20 | obj_uvmaps = obj.data.uv_layers
21 | if obj_uvmaps and name in obj_uvmaps:
22 | obj_uvmaps.remove(obj_uvmaps[name])
23 |
24 |
25 |
26 | def active_uvmap_by_active(obj, index):
27 | obj_uvmaps = obj.data.uv_layers
28 | if len(obj_uvmaps) >= index + 1:
29 | obj_uvmaps.active_index = index
30 |
31 |
32 |
33 | class IOPS_OT_Add_UVMap(bpy.types.Operator):
34 | """Add UVMap to selected objects"""
35 |
36 | bl_idname = "iops.uv_add_uvmap"
37 | bl_label = "Add UVMap to selected objects"
38 | bl_options = {"REGISTER", "UNDO"}
39 |
40 | def execute(self, context):
41 | selected_objs = [
42 | o for o in context.view_layer.objects.selected
43 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
44 | ]
45 | if selected_objs:
46 | #check for instances
47 | unique_objects = {}
48 | filtered_list = []
49 | for ob in selected_objs:
50 | # Check if the object is an instance (linked data)
51 | ob_data= ob.data
52 | if ob_data not in unique_objects:
53 | unique_objects[ob_data] = ob
54 | filtered_list.append(ob)
55 | for ob in filtered_list:
56 | uvmap_add(ob)
57 | self.report({"INFO"}, "UVMaps Were Added")
58 | else:
59 | self.report({"ERROR"}, "Select MESH objects.")
60 | return {"FINISHED"}
61 |
62 |
63 | class IOPS_OT_Remove_UVMap_by_Active_Name(bpy.types.Operator):
64 | """Remove UVMap by Name of Active UVMap on selected objects"""
65 |
66 | bl_idname = "iops.uv_remove_uvmap_by_active_name"
67 | bl_label = "Remove UVMap by Name of Active UVMap on selected objects"
68 | bl_options = {"REGISTER", "UNDO"}
69 |
70 | def execute(self, context):
71 | selected_objs = [
72 | o
73 | for o in context.view_layer.objects.selected
74 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
75 | ]
76 | if selected_objs and context.active_object in selected_objs:
77 | if context.active_object.data.uv_layers:
78 | uvmap_name = context.active_object.data.uv_layers.active.name
79 | for ob in selected_objs:
80 | if ob.data.uv_layers:
81 | uvmap_clean_by_name(ob, uvmap_name)
82 | tag_redraw(context)
83 | self.report({"INFO"}, ("UVMap %s Was Deleted" % (uvmap_name)))
84 | else:
85 | self.report({"ERROR"}, "Select MESH objects.")
86 | return {"FINISHED"}
87 |
88 |
89 | class IOPS_OT_Active_UVMap_by_Active(bpy.types.Operator):
90 | """Remove UVMap by Name of Active UVMap on selected objects"""
91 |
92 | bl_idname = "iops.uv_active_uvmap_by_active_object"
93 | bl_label = "Set Active UVMap as Active object active UVMap"
94 | bl_options = {"REGISTER", "UNDO"}
95 |
96 | def execute(self, context):
97 | selected_objs = [
98 | o
99 | for o in context.view_layer.objects.selected
100 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
101 | ]
102 | if selected_objs and context.active_object in selected_objs:
103 | if context.active_object.data.uv_layers:
104 | uvmap_name = context.active_object.data.uv_layers.active.name
105 | uvmap_index = context.active_object.data.uv_layers.active_index
106 | for ob in selected_objs:
107 | if ob.data.uv_layers:
108 | active_uvmap_by_active(ob, uvmap_index)
109 | tag_redraw(context)
110 | self.report({"INFO"}, ("UVMap %s Active" % (uvmap_name)))
111 | else:
112 | self.report({"ERROR"}, "Select MESH objects.")
113 | return {"FINISHED"}
114 |
--------------------------------------------------------------------------------
/operators/object_uvmaps_cleaner.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | # from mathutils import Vector, Matrix
4 | # from bpy.props import (BoolProperty,
5 | # EnumProperty,
6 | # FloatProperty,
7 | # IntProperty,
8 | # PointerProperty,
9 | # StringProperty,
10 | # FloatVectorProperty,
11 | # )
12 |
13 |
14 | def tag_redraw(context, space_type="VIEW_3D", region_type="WINDOW"):
15 | for window in context.window_manager.windows:
16 | for area in window.screen.areas:
17 | if area.spaces[0].type == space_type:
18 | for region in area.regions:
19 | if region.type == region_type:
20 | region.tag_redraw()
21 |
22 | def uvmap_clean_by_index(obj, index):
23 | obj_uvmaps = obj.data.uv_layers
24 | if obj_uvmaps:
25 | ch_num = len(obj_uvmaps)
26 | for i in range(ch_num, index, -1):
27 | if i != 1:
28 | obj_uvmaps.remove(obj_uvmaps[i - 1])
29 | else:
30 | obj_uvmaps.remove(obj_uvmaps[0])
31 |
32 |
33 | class IOPS_OT_Clean_UVMap_0(bpy.types.Operator):
34 | """Clean all UVMaps on selected objects"""
35 |
36 | bl_idname = "iops.object_clean_uvmap_0"
37 | bl_label = "Remove All UVMaps"
38 | bl_options = {"REGISTER", "UNDO"}
39 |
40 | # @classmethod
41 | # def poll(cls, context):
42 | # return (context.area.type == "VIEW_3D")
43 |
44 | def execute(self, context):
45 | selected_objs = [
46 | o
47 | for o in context.view_layer.objects.selected
48 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
49 | ]
50 | if selected_objs:
51 | for ob in selected_objs:
52 | uvmap_clean_by_index(ob, 0)
53 | tag_redraw(context)
54 | self.report({"INFO"}, "All UVMaps Were Removed")
55 |
56 | else:
57 | self.report({"ERROR"}, "Select MESH objects.")
58 | return {"FINISHED"}
59 |
60 |
61 | class IOPS_OT_Clean_UVMap_1(bpy.types.Operator):
62 | """Clean from UVMap #2 and up - on selected objects"""
63 |
64 | bl_idname = "iops.object_clean_uvmap_1"
65 | bl_label = "Remove UVMap 1"
66 | bl_options = {"REGISTER", "UNDO"}
67 |
68 | # @classmethod
69 | # def poll(cls, context):
70 | # return (context.area.type == "VIEW_3D")
71 |
72 | def execute(self, context):
73 | selected_objs = [
74 | o
75 | for o in context.view_layer.objects.selected
76 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
77 | ]
78 | if selected_objs:
79 | for ob in selected_objs:
80 | uvmap_clean_by_index(ob, 1)
81 | tag_redraw(context)
82 | self.report({"INFO"}, "UVMaps 2 to 8 Removed")
83 |
84 | else:
85 | self.report({"ERROR"}, "Select MESH objects.")
86 | return {"FINISHED"}
87 |
88 |
89 | class IOPS_OT_Clean_UVMap_2(bpy.types.Operator):
90 | """Clean from UVMap #3 and up - on selected objects"""
91 |
92 | bl_idname = "iops.object_clean_uvmap_2"
93 | bl_label = "Remove UVMap 2"
94 | bl_options = {"REGISTER", "UNDO"}
95 |
96 | # @classmethod
97 | # def poll(cls, context):
98 | # return (context.area.type == "VIEW_3D")
99 |
100 | def execute(self, context):
101 | selected_objs = [
102 | o
103 | for o in context.view_layer.objects.selected
104 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
105 | ]
106 | if selected_objs:
107 | for ob in selected_objs:
108 | uvmap_clean_by_index(ob, 2)
109 | tag_redraw(context)
110 | self.report({"INFO"}, "UVMaps 3 to 8 Removed")
111 |
112 | else:
113 | self.report({"ERROR"}, "Select MESH objects.")
114 | return {"FINISHED"}
115 |
116 |
117 | class IOPS_OT_Clean_UVMap_3(bpy.types.Operator):
118 | """Clean from UVMap #4 and up - on selected objects"""
119 |
120 | bl_idname = "iops.object_clean_uvmap_3"
121 | bl_label = "Remove UVMap 3"
122 | bl_options = {"REGISTER", "UNDO"}
123 |
124 | # @classmethod
125 | # def poll(cls, context):
126 | # return (context.area.type == "VIEW_3D")
127 |
128 | def execute(self, context):
129 | selected_objs = [
130 | o
131 | for o in context.view_layer.objects.selected
132 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
133 | ]
134 | if selected_objs:
135 | for ob in selected_objs:
136 | uvmap_clean_by_index(ob, 3)
137 | tag_redraw(context)
138 | self.report({"INFO"}, "UVMaps 4 to 8 Removed")
139 |
140 | else:
141 | self.report({"ERROR"}, "Select MESH objects.")
142 | return {"FINISHED"}
143 |
144 |
145 | class IOPS_OT_Clean_UVMap_4(bpy.types.Operator):
146 | """Clean from UVMap #5 and up - on selected objects"""
147 |
148 | bl_idname = "iops.object_clean_uvmap_4"
149 | bl_label = "Remove UVMap 4"
150 | bl_options = {"REGISTER", "UNDO"}
151 |
152 | # @classmethod
153 | # def poll(cls, context):
154 | # return (context.area.type == "VIEW_3D")
155 |
156 | def execute(self, context):
157 | selected_objs = [
158 | o
159 | for o in context.view_layer.objects.selected
160 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
161 | ]
162 | if selected_objs:
163 | for ob in selected_objs:
164 | uvmap_clean_by_index(ob, 4)
165 | tag_redraw(context)
166 | self.report({"INFO"}, "UVMaps 5 to 8 Removed")
167 |
168 | else:
169 | self.report({"ERROR"}, "Select MESH objects.")
170 | return {"FINISHED"}
171 |
172 |
173 | class IOPS_OT_Clean_UVMap_5(bpy.types.Operator):
174 | """Clean from UVMap #6 and up - on selected objects"""
175 |
176 | bl_idname = "iops.object_clean_uvmap_5"
177 | bl_label = "Remove UVMap 5"
178 | bl_options = {"REGISTER", "UNDO"}
179 |
180 | # @classmethod
181 | # def poll(cls, context):
182 | # return (context.area.type == "VIEW_3D")
183 |
184 | def execute(self, context):
185 | selected_objs = [
186 | o
187 | for o in context.view_layer.objects.selected
188 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
189 | ]
190 | if selected_objs:
191 | for ob in selected_objs:
192 | uvmap_clean_by_index(ob, 5)
193 | tag_redraw(context)
194 | self.report({"INFO"}, "UVMaps 6 to 8 Removed")
195 |
196 | else:
197 | self.report({"ERROR"}, "Select MESH objects.")
198 | return {"FINISHED"}
199 |
200 |
201 | class IOPS_OT_Clean_UVMap_6(bpy.types.Operator):
202 | """Clean from UVMap #7 and up - on selected objects"""
203 |
204 | bl_idname = "iops.object_clean_uvmap_6"
205 | bl_label = "Remove UVMap 6"
206 | bl_options = {"REGISTER", "UNDO"}
207 |
208 | # @classmethod
209 | # def poll(cls, context):
210 | # return (context.area.type == "VIEW_3D")
211 |
212 | def execute(self, context):
213 | selected_objs = [
214 | o
215 | for o in context.view_layer.objects.selected
216 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
217 | ]
218 | if selected_objs:
219 | for ob in selected_objs:
220 | uvmap_clean_by_index(ob, 6)
221 | tag_redraw(context)
222 | self.report({"INFO"}, "UVMaps 7 to 8 Removed")
223 |
224 | else:
225 | self.report({"ERROR"}, "Select MESH objects.")
226 | return {"FINISHED"}
227 |
228 |
229 | class IOPS_OT_Clean_UVMap_7(bpy.types.Operator):
230 | """Clean UVMap #8 - on selected objects"""
231 |
232 | bl_idname = "iops.object_clean_uvmap_7"
233 | bl_label = "Remove UVMap 7"
234 | bl_options = {"REGISTER", "UNDO"}
235 |
236 | # @classmethod
237 | # def poll(cls, context):
238 | # return (context.area.type == "VIEW_3D")
239 |
240 | def execute(self, context):
241 | selected_objs = [
242 | o
243 | for o in context.view_layer.objects.selected
244 | if o.type == "MESH" and o.data.polygons[:] != [] and o.visible_get()
245 | ]
246 | if selected_objs:
247 | for ob in selected_objs:
248 | uvmap_clean_by_index(ob, 7)
249 | tag_redraw(context)
250 | self.report({"INFO"}, "UVMap 8 Removed")
251 |
252 | else:
253 | self.report({"ERROR"}, "Select MESH objects.")
254 | return {"FINISHED"}
255 |
--------------------------------------------------------------------------------
/operators/open_asset_in_current_blender.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | class IOPS_OT_OpenAssetInCurrentBlender(bpy.types.Operator):
5 | """Open asset in current Blender instance"""
6 |
7 | bl_idname = "iops.open_asset_in_current_blender"
8 | bl_label = "Open in Current Blender"
9 | bl_options = {"REGISTER"}
10 |
11 |
12 | @classmethod
13 | def poll(cls, context):
14 | asset = getattr(context, "asset", None)
15 |
16 | if not asset:
17 | cls.poll_message_set("No asset selected")
18 | return False
19 | if asset.local_id:
20 | cls.poll_message_set("Selected asset is contained in the current file")
21 | return False
22 | # This could become a built-in query, for now this is good enough.
23 | if asset.full_library_path.endswith(".asset.blend"):
24 | cls.poll_message_set(
25 | "Selected asset is contained in a file managed by the asset system, manual edits should be avoided",
26 | )
27 | return False
28 | return True
29 |
30 | def execute(self, context):
31 | asset = context.asset
32 |
33 | if asset.local_id:
34 | self.report({'WARNING'}, "This asset is stored in the current blend file")
35 | return {'CANCELLED'}
36 |
37 | asset_lib_path = asset.full_library_path
38 | bpy.ops.wm.open_mainfile(filepath=asset_lib_path)
39 |
40 | return {'FINISHED'}
41 |
--------------------------------------------------------------------------------
/operators/outliner_collection_ops.py:
--------------------------------------------------------------------------------
1 | # Include/Exclude collections from view layer
2 |
3 | import bpy
4 |
5 |
6 | def exclude_layer_col_by_name(layerColl, collName, exclude):
7 | found = None
8 | if layerColl.name == collName:
9 | layerColl.exclude = exclude
10 | return layerColl
11 | for layer in layerColl.children:
12 | found = exclude_layer_col_by_name(layer, collName, exclude)
13 | if found:
14 | found.exclude = exclude
15 | return found
16 |
17 |
18 | class IOPS_OT_Collections_Include(bpy.types.Operator):
19 | """Include collection and children in view layer"""
20 |
21 | bl_idname = "iops.collections_include"
22 | bl_label = "Include All"
23 | bl_description = "Include collection and children in view layer"
24 | bl_options = {"REGISTER", "UNDO"}
25 |
26 | def execute(self, context):
27 | selected_cols = [
28 | col
29 | for col in bpy.context.selected_ids
30 | if isinstance(col, bpy.types.Collection)
31 | ]
32 | selected_cols_children = []
33 |
34 | # Get all children from selected_cols withoud duplicates
35 | for col in selected_cols:
36 | for child in col.children_recursive:
37 | if child not in selected_cols_children:
38 | selected_cols_children.append(child)
39 |
40 | all_colls = selected_cols + selected_cols_children
41 |
42 | master_col = bpy.context.view_layer.layer_collection
43 | # Get layer collection from selected_cols names
44 | for col in all_colls:
45 | exclude_layer_col_by_name(master_col, col.name, False)
46 |
47 | return {"FINISHED"}
48 |
49 |
50 | class IOPS_OT_Collections_Exclude(bpy.types.Operator):
51 | """Exclude collection and children from view layer"""
52 |
53 | bl_idname = "iops.collections_exclude"
54 | bl_label = "Exclude All"
55 | bl_description = "Exclude collection and children from view layer"
56 | bl_options = {"REGISTER", "UNDO"}
57 |
58 | def execute(self, context):
59 | selected_cols = [
60 | col
61 | for col in bpy.context.selected_ids
62 | if isinstance(col, bpy.types.Collection)
63 | ]
64 | selected_cols_children = []
65 |
66 | # Get all children from selected_cols withoud duplicates
67 | for col in selected_cols:
68 | for child in col.children_recursive:
69 | if child not in selected_cols_children:
70 | selected_cols_children.append(child)
71 |
72 | all_colls = selected_cols + selected_cols_children
73 |
74 | master_col = bpy.context.view_layer.layer_collection
75 | # Get layer collection from selected_cols names
76 | for col in all_colls:
77 | exclude_layer_col_by_name(master_col, col.name, True)
78 |
79 | return {"FINISHED"}
80 |
--------------------------------------------------------------------------------
/operators/preferences/io_addon_preferences.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | import json
4 | from ...prefs.iops_prefs import get_iops_prefs
5 | from ...utils.split_areas_dict import split_areas_dict, split_areas_position_list
6 |
7 | # Save Addon Preferences
8 | def save_iops_preferences():
9 | iops_prefs = get_iops_prefs()
10 | path = bpy.utils.script_path_user()
11 | folder = os.path.join(path, "presets", "IOPS")
12 | iops_prefs_file = os.path.join(path, "presets", "IOPS", "iops_prefs_user.json")
13 |
14 | if not os.path.exists(folder):
15 | os.makedirs(folder)
16 |
17 | with open(iops_prefs_file, "w") as f:
18 | json.dump(iops_prefs, f, indent=4)
19 |
20 |
21 | def get_split_pos_ui(pos, ui):
22 | for p in split_areas_position_list:
23 | if p[0] == pos:
24 | pos_num = p[4]
25 | for key, value in split_areas_dict.items():
26 | if value["ui"] == ui:
27 | ui_num = value["num"]
28 | return pos_num, ui_num
29 |
30 |
31 | # Load Addon Preferences
32 | def load_iops_preferences():
33 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
34 | path = bpy.utils.script_path_user()
35 | try:
36 | iops_prefs_file = os.path.join(path, "presets", "IOPS", "iops_prefs_user.json")
37 | with open(iops_prefs_file, "r") as f:
38 | iops_prefs = json.load(f)
39 | for key, value in iops_prefs.items():
40 | match key:
41 | case "IOPS_DEBUG":
42 | prefs.IOPS_DEBUG = value["IOPS_DEBUG"]
43 | case "ALIGN_TO_EDGE":
44 | prefs.align_edge_color = value["align_edge_color"]
45 | case "EXECUTOR":
46 | prefs.executor_scripts_folder = value["executor_scripts_folder"]
47 | prefs.executor_column_count = value["executor_column_count"]
48 | prefs.executor_name_lenght = value["executor_name_lenght"]
49 | prefs.executor_use_script_path_user = value["executor_use_script_path_user"]
50 | prefs.executor_scripts_subfolder = value["executor_scripts_subfolder"]
51 | case "SPLIT_AREA_PIES":
52 | for pie in value:
53 | pie_num = pie[-1]
54 | pos, ui = get_split_pos_ui(
55 | value[pie][f"split_area_pie_{pie_num}_pos"],
56 | value[pie][f"split_area_pie_{pie_num}_ui"],
57 | )
58 | prefs[f"split_area_pie_{pie_num}_factor"] = value[pie][
59 | f"split_area_pie_{pie_num}_factor"
60 | ]
61 | prefs[f"split_area_pie_{pie_num}_pos"] = pos
62 | prefs[f"split_area_pie_{pie_num}_ui"] = ui
63 | case "UI_TEXT":
64 | prefs.text_color = value["text_color"]
65 | prefs.text_color_key = value["text_color_key"]
66 | prefs.text_pos_x = value["text_pos_x"]
67 | prefs.text_pos_y = value["text_pos_y"]
68 | prefs.text_shadow_color = value["text_shadow_color"]
69 | prefs.text_shadow_pos_x = value["text_shadow_pos_x"]
70 | prefs.text_shadow_pos_y = value["text_shadow_pos_y"]
71 | prefs.text_shadow_toggle = value["text_shadow_toggle"]
72 | prefs.text_size = value["text_size"]
73 | case "UI_TEXT_STAT":
74 | prefs.iops_stat = value["iops_stat"]
75 | prefs.text_color_stat = value["text_color_stat"]
76 | prefs.text_color_key_stat = value["text_color_key_stat"]
77 | prefs.text_color_error_stat = value["text_color_error_stat"]
78 | prefs.text_pos_x_stat = value["text_pos_x_stat"]
79 | prefs.text_pos_y_stat = value["text_pos_y_stat"]
80 | prefs.text_shadow_color_stat = value["text_shadow_color_stat"]
81 | prefs.text_shadow_pos_x_stat = value["text_shadow_pos_x_stat"]
82 | prefs.text_shadow_pos_y_stat = value["text_shadow_pos_y_stat"]
83 | prefs.text_shadow_toggle_stat = value["text_shadow_toggle_stat"]
84 | prefs.text_size_stat = value["text_size_stat"]
85 | prefs.text_column_offset_stat = value["text_column_offset_stat"]
86 | prefs.text_column_width_stat = value["text_column_width_stat"]
87 | case "VISUAL_ORIGIN":
88 | prefs.vo_cage_ap_color = value["vo_cage_ap_color"]
89 | prefs.vo_cage_ap_size = value["vo_cage_ap_size"]
90 | prefs.vo_cage_color = value["vo_cage_color"]
91 | prefs.vo_cage_p_size = value["vo_cage_p_size"]
92 | prefs.vo_cage_points_color = value["vo_cage_points_color"]
93 | prefs.vo_cage_line_thickness = value["vo_cage_line_thickness"]
94 | case "TEXTURE_TO_MATERIAL":
95 | prefs.texture_to_material_prefixes = value[
96 | "texture_to_material_prefixes"
97 | ]
98 | prefs.texture_to_material_suffixes = value[
99 | "texture_to_material_suffixes"
100 | ]
101 | case "SNAP_COMBOS":
102 | for i in range(1, 9):
103 | prefs[f"snap_combo_{i}"] = value[f"snap_combo_{i}"]
104 | case "DRAG_SNAP":
105 | prefs.drag_snap_line_thickness = value["drag_snap_line_thickness"]
106 | case _:
107 | print(
108 | "IOPS Prefs: No entry for " + key,
109 | )
110 |
111 | except FileNotFoundError:
112 | save_iops_preferences()
113 | print("IOPS Preferences file was not found. A new one was created at:", iops_prefs_file)
114 | return
115 |
116 |
117 | class IOPS_OT_SaveAddonPreferences(bpy.types.Operator):
118 | bl_idname = "iops.save_addon_preferences"
119 | bl_label = "Save Addon Preferences"
120 | bl_options = {"REGISTER", "UNDO"}
121 |
122 | def execute(self, context):
123 | save_iops_preferences()
124 | print("IOPS Preferences were Saved.")
125 | return {"FINISHED"}
126 |
127 |
128 | class IOPS_OT_LoadAddonPreferences(bpy.types.Operator):
129 | bl_idname = "iops.load_addon_preferences"
130 | bl_label = "Load Addon Preferences"
131 | bl_options = {"REGISTER", "UNDO"}
132 |
133 | def execute(self, context):
134 | load_iops_preferences()
135 | print("IOPS Preferences were Loaded")
136 | return {"FINISHED"}
137 |
--------------------------------------------------------------------------------
/operators/render_asset_thumbnail.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.props import FloatProperty, BoolProperty, EnumProperty
3 | import os
4 |
5 |
6 | def get_path():
7 | return os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
8 |
9 |
10 | class IOPS_OT_RenderAssetThumbnail(bpy.types.Operator):
11 | bl_idname = "iops.assets_render_asset_thumbnail"
12 | bl_label = "Render Active Asset Thumbnail"
13 | bl_description = "Render Active Asset Thumbnail: Collection, Object, Material, Geometry Nodes"
14 | bl_options = {"REGISTER", "UNDO"}
15 |
16 | thumbnail_lens: FloatProperty(name="Thumbnail Lens", default=100)
17 | toggle_overlays: BoolProperty(name="Toggle Overlays", default=True)
18 |
19 | render_for: EnumProperty(
20 | name="Render for:",
21 | items=[
22 | ("OBJECT", "Object", "Render the selected object"),
23 | ("COLLECTION", "Collection", "Render the selected collection"),
24 | ("MATERIAL", "Material", "Render the selected material"),
25 | ("GEOMETRY", "Geometry Nodes", "Render the selected geometry node"),
26 |
27 | ],
28 | default="COLLECTION",
29 | )
30 |
31 | def render_viewport(self, context, filepath):
32 | resolution = (
33 | context.scene.render.resolution_x,
34 | context.scene.render.resolution_y,
35 | )
36 | file_format = context.scene.render.image_settings.file_format
37 | lens = context.space_data.lens
38 | show_overlays = context.space_data.overlay.show_overlays
39 |
40 | context.scene.render.resolution_x = 500
41 | context.scene.render.resolution_y = 500
42 | context.scene.render.image_settings.file_format = "JPEG"
43 |
44 | context.space_data.lens = self.thumbnail_lens
45 |
46 | if show_overlays and self.toggle_overlays:
47 | context.space_data.overlay.show_overlays = False
48 |
49 | bpy.ops.render.opengl()
50 |
51 | thumb = bpy.data.images.get("Render Result")
52 |
53 | if thumb:
54 | thumb.save_render(filepath=filepath)
55 |
56 | context.scene.render.resolution_x = resolution[0]
57 | context.scene.render.resolution_y = resolution[1]
58 | context.space_data.lens = lens
59 |
60 | context.scene.render.image_settings.file_format = file_format
61 |
62 | if show_overlays and self.toggle_overlays:
63 | context.space_data.overlay.show_overlays = True
64 |
65 | def execute(self, context):
66 | if self.render_for == "COLLECTION":
67 | active_collection = bpy.context.collection
68 |
69 | thumbpath = os.path.join(get_path(), "resources", "thumb.png")
70 | self.render_viewport(context, thumbpath)
71 | if os.path.exists(thumbpath):
72 | with context.temp_override(id=active_collection):
73 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath)
74 | os.unlink(thumbpath)
75 | elif self.render_for == "OBJECT":
76 | active_object = bpy.context.object
77 |
78 | thumbpath = os.path.join(get_path(), "resources", "thumb.png")
79 | self.render_viewport(context, thumbpath)
80 | if os.path.exists(thumbpath):
81 | with context.temp_override(id=active_object):
82 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath)
83 | os.unlink(thumbpath)
84 | elif self.render_for == "MATERIAL":
85 | active_object = bpy.context.object
86 | if active_object.type == "MESH":
87 | active_material = active_object.active_material
88 |
89 | thumbpath = os.path.join(get_path(), "resources", "thumb.png")
90 | self.render_viewport(context, thumbpath)
91 | if os.path.exists(thumbpath):
92 | try:
93 | with context.temp_override(id=active_material):
94 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath)
95 | os.unlink(thumbpath)
96 | except RuntimeError:
97 | self.report({"ERROR"}, "Current object does not have a material marked as asset")
98 | elif self.render_for == "GEOMETRY":
99 | active_object = bpy.context.object
100 | if active_object.type == "MESH" and active_object.modifiers.active.type == "NODES":
101 | active_node = active_object.modifiers.active.node_group
102 |
103 | thumbpath = os.path.join(get_path(), "resources", "thumb.png")
104 | self.render_viewport(context, thumbpath)
105 | if os.path.exists(thumbpath):
106 | try:
107 | with context.temp_override(id=active_node):
108 | bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbpath)
109 | os.unlink(thumbpath)
110 | except RuntimeError:
111 | self.report({"ERROR"}, "Current object does not have a node groups marked as asset")
112 | else:
113 | self.report({"ERROR"}, "Active object is not a mesh")
114 |
115 | return {"FINISHED"}
116 |
--------------------------------------------------------------------------------
/operators/run_text.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | def ContextOverride():
5 | for window in bpy.context.window_manager.windows:
6 | screen = window.screen
7 | for area in screen.areas:
8 | if area.type == "TEXT_EDITOR":
9 | for region in area.regions:
10 | if region.type == "WINDOW":
11 | context_override = {
12 | "window": window,
13 | "screen": screen,
14 | "area": area,
15 | "region": region,
16 | "scene": bpy.context.scene,
17 | "edit_object": bpy.context.edit_object,
18 | "active_object": bpy.context.active_object,
19 | "selected_objects": bpy.context.selected_objects,
20 | }
21 | return context_override
22 | raise Exception("ERROR: TEXT_EDITOR not found!")
23 |
24 |
25 | class IOPS_OT_RunText(bpy.types.Operator):
26 | """Run Current Script in Text Editor"""
27 |
28 | bl_idname = "iops.scripts_run_text"
29 | bl_label = "IOPS Run Text"
30 | bl_options = {"REGISTER", "UNDO"}
31 |
32 | def execute(self, context):
33 | context_override = ContextOverride()
34 | with context.temp_override(**context_override):
35 | bpy.ops.text.run_script()
36 | return {"FINISHED"}
37 |
--------------------------------------------------------------------------------
/operators/save_load_space_data.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | def save_space_data(context):
5 | area_type = context.area.type
6 | sd = context.space_data
7 |
8 | if "IOPS" not in context.scene:
9 | context.scene["IOPS"] = {}
10 |
11 | if area_type not in context.scene["IOPS"]:
12 | context.scene["IOPS"][area_type] = {}
13 |
14 | if area_type == "OUTLINER":
15 | outliner = context.scene["IOPS"]["OUTLINER"]
16 | display_mode = context.space_data.display_mode
17 | outliner["display_mode"] = display_mode
18 |
19 | outliner["show_header"] = sd.show_region_header
20 | outliner["show_menus"] = context.area.show_menus
21 |
22 | if display_mode == "VIEW_LAYER":
23 | outliner["show_restrict_column_enable"] = sd.show_restrict_column_enable
24 | outliner["show_restrict_column_select"] = sd.show_restrict_column_select
25 | outliner["show_restrict_column_hide"] = sd.show_restrict_column_hide
26 | outliner["show_restrict_column_viewport"] = sd.show_restrict_column_viewport
27 | outliner["show_restrict_column_render"] = sd.show_restrict_column_render
28 | outliner["show_restrict_column_holdout"] = sd.show_restrict_column_holdout
29 |
30 | elif display_mode == "SCENES":
31 | outliner["show_restrict_column_select"] = sd.show_restrict_column_select
32 | outliner["show_restrict_column_hide"] = sd.show_restrict_column_hide
33 | outliner["show_restrict_column_viewport"] = sd.show_restrict_column_viewport
34 | outliner["show_restrict_column_render"] = sd.show_restrict_column_render
35 |
36 | if display_mode != "DATA_API":
37 | outliner["use_sort_alpha"] = sd.use_sort_alpha
38 | else:
39 | context.scene["IOPS"][area_type]["show_header"] = sd.show_region_header
40 | context.scene["IOPS"][area_type]["show_menus"] = context.area.show_menus
41 |
42 |
43 | def load_space_data(context):
44 | area_type = context.area.type
45 | sd = context.space_data
46 |
47 | if "IOPS" not in context.scene:
48 | context.scene["IOPS"] = {}
49 |
50 | if context.scene["IOPS"].values() != []:
51 | if area_type in context.scene["IOPS"] and area_type == "OUTLINER":
52 | outliner = context.scene["IOPS"]["OUTLINER"]
53 | display_mode = context.space_data.display_mode
54 | sd.display_mode = outliner["display_mode"]
55 | context.area.show_menus = outliner["show_menus"]
56 | sd.show_region_header = outliner["show_header"]
57 |
58 | if display_mode == "VIEW_LAYER":
59 | sd.show_restrict_column_enable = outliner["show_restrict_column_enable"]
60 | sd.show_restrict_column_select = outliner["show_restrict_column_select"]
61 | sd.show_restrict_column_hide = outliner["show_restrict_column_hide"]
62 | sd.show_restrict_column_viewport = outliner[
63 | "show_restrict_column_viewport"
64 | ]
65 | sd.show_restrict_column_render = outliner["show_restrict_column_render"]
66 | sd.show_restrict_column_holdout = outliner[
67 | "show_restrict_column_holdout"
68 | ]
69 |
70 | elif display_mode == "SCENES":
71 | sd.show_restrict_column_select = outliner["show_restrict_column_select"]
72 | sd.show_restrict_column_hide = outliner["show_restrict_column_hide"]
73 | sd.show_restrict_column_viewport = outliner[
74 | "show_restrict_column_viewport"
75 | ]
76 | sd.show_restrict_column_render = outliner["show_restrict_column_render"]
77 |
78 | if display_mode != "DATA_API":
79 | sd.use_sort_alpha = outliner["use_sort_alpha"]
80 |
81 | return f"{area_type} space data loaded"
82 |
83 | elif area_type in context.scene["IOPS"]:
84 | sd.show_region_header = context.scene["IOPS"][area_type]["show_header"]
85 | context.area.show_menus = context.scene["IOPS"][area_type]["show_menus"]
86 | return f"{area_type} space data loaded"
87 | else:
88 | return f"No space data for {area_type}"
89 | else:
90 | return "No space data to load!"
91 |
92 |
93 | class IOPS_OT_SaveSpaceData(bpy.types.Operator):
94 | """Save Space Data"""
95 |
96 | bl_idname = "iops.space_data_save"
97 | bl_label = "Save space_data"
98 |
99 | # @classmethod
100 | # def poll(cls, context):
101 | # return context.area.type == 'OUTLINER'
102 |
103 | def execute(self, context):
104 | save_space_data(context)
105 | self.report({"INFO"}, f"{bpy.context.area.type} Space Data Saved")
106 | return {"FINISHED"}
107 |
108 |
109 | class IOPS_OT_LoadSpaceData(bpy.types.Operator):
110 | """Save Space Data"""
111 |
112 | bl_idname = "iops.space_data_load"
113 | bl_label = "Load space_data"
114 |
115 | # @classmethod
116 | # def poll(cls, context):
117 | # return context.area.type == 'OUTLINER'
118 |
119 | def execute(self, context):
120 | event = load_space_data(context)
121 | if event:
122 | self.report({"INFO"}, event)
123 | else:
124 | self.report({"INFO"}, "load_space_data failed")
125 | return {"FINISHED"}
126 |
--------------------------------------------------------------------------------
/operators/snap_combos.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | import json
4 |
5 | def ensure_snap_combos_prefs():
6 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
7 |
8 | for i in range(1,9):
9 | if f"snap_combo_{i}" not in prefs.keys():
10 | prefs[f"snap_combo_{i}"] = {
11 | "SNAP_ELEMENTS": {
12 | "INCREMENT": False,
13 | "VERTEX": True,
14 | "EDGE": False,
15 | "FACE": False,
16 | "VOLUME": False,
17 | "EDGE_MIDPOINT": False,
18 | "EDGE_PERPENDICULAR": False,
19 | "FACE_PROJECT": False,
20 | "FACE_NEAREST": False
21 | },
22 | "TOOL_SETTINGS": {
23 | "transform_pivot_point": "ACTIVE_ELEMENT",
24 | "snap_target": "ACTIVE",
25 | # "use_snap_grid_absolute": True,
26 | "use_snap_self": True,
27 | "use_snap_align_rotation": False,
28 | "use_snap_peel_object": True,
29 | "use_snap_backface_culling": False,
30 | "use_snap_selectable": False,
31 | "use_snap_translate": False,
32 | "use_snap_rotate": False,
33 | "use_snap_scale": False,
34 | "use_snap_to_same_target": False
35 | },
36 | "TRANSFORMATION": "GLOBAL"
37 | }
38 |
39 | def save_snap_combo(idx):
40 | tool_settings = bpy.context.scene.tool_settings
41 | prefs = bpy.context.preferences.addons["InteractionOps"].preferences
42 | path = bpy.utils.script_path_user()
43 | iops_prefs_file = os.path.join(path, "presets", "IOPS", "iops_prefs_user.json")
44 |
45 | snap_elements_list = ['VERTEX',
46 | 'EDGE',
47 | 'FACE',
48 | 'VOLUME',
49 | 'INCREMENT',
50 | 'EDGE_MIDPOINT',
51 | 'EDGE_PERPENDICULAR',
52 | 'FACE_PROJECT',
53 | 'FACE_NEAREST'
54 | ]
55 |
56 | with open(iops_prefs_file, "r") as f:
57 | iops_prefs = json.load(f)
58 |
59 | ensure_snap_combos_prefs()
60 |
61 | for snap_combo, snap_details in iops_prefs.get("SNAP_COMBOS", {}).items():
62 | if snap_combo[-1] == str(idx):
63 | snap_elements = tool_settings.snap_elements
64 | snap_elements_dict = {k: k in snap_elements for k in snap_elements_list}
65 | tool_settings_dict = {
66 | "transform_pivot_point": tool_settings.transform_pivot_point,
67 | "snap_target": tool_settings.snap_target,
68 | # "use_snap_grid_absolute": tool_settings.use_snap_grid_absolute,
69 | "use_snap_self": tool_settings.use_snap_self,
70 | "use_snap_align_rotation": tool_settings.use_snap_align_rotation,
71 | "use_snap_peel_object": tool_settings.use_snap_peel_object,
72 | "use_snap_backface_culling": tool_settings.use_snap_backface_culling,
73 | "use_snap_selectable": tool_settings.use_snap_selectable,
74 | "use_snap_translate": tool_settings.use_snap_translate,
75 | "use_snap_rotate": tool_settings.use_snap_rotate,
76 | "use_snap_scale": tool_settings.use_snap_scale,
77 | "use_snap_to_same_target": tool_settings.use_snap_to_same_target
78 | }
79 | snap_details["SNAP_ELEMENTS"] = snap_elements_dict
80 | snap_details["TOOL_SETTINGS"] = tool_settings_dict
81 | snap_details["TRANSFORMATION"] = bpy.context.scene.transform_orientation_slots[0].type
82 | prefs[snap_combo] = snap_details
83 | break
84 |
85 | with open(iops_prefs_file, "w") as f:
86 | json.dump(iops_prefs, f, indent=4)
87 |
88 |
89 |
90 | class IOPS_OT_SetSnapCombo(bpy.types.Operator):
91 | '''
92 | Click to set the Snap Combo
93 | Shift+Click to save the current Snap Combo
94 | '''
95 | bl_idname = "iops.set_snap_combo"
96 | bl_label = "Set Snap Combo"
97 | bl_options = {"REGISTER", "UNDO"}
98 |
99 | idx: bpy.props.IntProperty()
100 |
101 | def invoke(self, context, event):
102 | prefs = context.preferences.addons["InteractionOps"].preferences
103 | save_combo_mod = prefs.snap_combo_mod
104 |
105 | if ((event.shift and save_combo_mod == "SHIFT") or
106 | (event.ctrl and save_combo_mod == "CTRL") or
107 | (event.alt and save_combo_mod == "ALT")):
108 |
109 | save_snap_combo(self.idx)
110 | self.report({"INFO"}, f"Snap Combo {self.idx} Saved.")
111 |
112 | else:
113 | tool_settings = context.scene.tool_settings
114 | snap_combos_prefs = [sc for sc in prefs.keys() if sc.startswith("snap_combo_")]
115 | for snap_combo in snap_combos_prefs:
116 | if snap_combo[-1] == str(self.idx):
117 | elements = {k for k, v in prefs[snap_combo]["SNAP_ELEMENTS"].items() if v}
118 | tool_settings.snap_elements = elements
119 | tool_settings.transform_pivot_point = prefs[snap_combo]["TOOL_SETTINGS"]["transform_pivot_point"]
120 | tool_settings.snap_target = prefs[snap_combo]["TOOL_SETTINGS"]["snap_target"]
121 | # tool_settings.use_snap_grid_absolute = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_grid_absolute"]
122 | tool_settings.use_snap_self = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_self"]
123 | tool_settings.use_snap_align_rotation = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_align_rotation"]
124 | tool_settings.use_snap_peel_object = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_peel_object"]
125 | tool_settings.use_snap_backface_culling = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_backface_culling"]
126 | tool_settings.use_snap_selectable = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_selectable"]
127 | tool_settings.use_snap_translate = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_translate"]
128 | tool_settings.use_snap_rotate = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_rotate"]
129 | tool_settings.use_snap_scale = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_scale"]
130 | tool_settings.use_snap_to_same_target = prefs[snap_combo]["TOOL_SETTINGS"]["use_snap_to_same_target"]
131 | bpy.context.scene.transform_orientation_slots[0].type = prefs[snap_combo]["TRANSFORMATION"]
132 | break
133 | self.report({"INFO"}, f"Snap Combo {self.idx} Loaded.")
134 | return {"FINISHED"}
135 |
136 |
--------------------------------------------------------------------------------
/operators/split_screen_area_new.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.props import StringProperty, FloatProperty
3 | import copy
4 |
5 |
6 | def ContextOverride(area):
7 | for window in bpy.context.window_manager.windows:
8 | screen = window.screen
9 | for screen_area in screen.areas:
10 | if screen_area.ui_type == area.ui_type:
11 | for region in screen_area.regions:
12 | if region.type == "WINDOW":
13 | context_override = {
14 | "window": window,
15 | "screen": screen,
16 | "area": screen_area,
17 | "region": region,
18 | }
19 | return context_override
20 | raise Exception("ERROR: Override failed!")
21 |
22 |
23 | class IOPS_OT_SplitScreenArea(bpy.types.Operator):
24 | bl_idname = "iops.split_screen_area"
25 | bl_label = "IOPS Split Screen Area"
26 |
27 | area_type: StringProperty(
28 | name="Area Type", description="Which area to create", default=""
29 | )
30 |
31 | ui: StringProperty(
32 | name="Area UI Type", description="Which UI to enable", default=""
33 | )
34 |
35 | pos: StringProperty(
36 | name="Position", description="Where to create new area", default=""
37 | )
38 |
39 | factor: FloatProperty(
40 | name="Factor",
41 | description="Area split factor",
42 | default=0.01,
43 | soft_min=0.01,
44 | soft_max=1,
45 | step=0.01,
46 | precision=2,
47 | )
48 |
49 | def refresh_ui(self, area):
50 | pass
51 |
52 | def get_join_xy(self, context, ui, pos):
53 | x, y = 0, 0
54 | if ui == context.area.ui_type:
55 | if pos == "TOP":
56 | x = int(context.area.width / 2 + context.area.x)
57 | y = context.area.y - 1
58 |
59 | elif pos == "RIGHT":
60 | x = context.area.x - 1
61 | y = int(context.area.height / 2 + context.area.y)
62 |
63 | elif pos == "BOTTOM":
64 | x = int(context.area.width / 2 + context.area.x)
65 | y = context.area.height + context.area.y + 1
66 |
67 | elif pos == "LEFT":
68 | x = context.area.width + context.area.x + 1
69 | y = int(context.area.y + context.area.height / 2)
70 |
71 | else:
72 | if pos == "TOP":
73 | x = int(context.area.x + context.area.width / 2)
74 | y = context.area.height + context.area.y + 1
75 |
76 | elif pos == "RIGHT":
77 | x = context.area.x + context.area.width + 1
78 | y = int(context.area.y + context.area.height / 2)
79 |
80 | elif pos == "BOTTOM":
81 | x = int(context.area.x + context.area.width / 2)
82 | y = context.area.y
83 |
84 | elif pos == "LEFT":
85 | x = context.area.x
86 | y = int(context.area.y + context.area.height / 2)
87 |
88 | return (x, y)
89 |
90 | def get_side_area(self, context, area, pos):
91 |
92 | side_area = None
93 |
94 | for screen_area in context.screen.areas:
95 | if (
96 | pos == "TOP"
97 | and screen_area.x == area.x
98 | and screen_area.width == area.width
99 | and screen_area.y == area.height + area.y + 1
100 | ):
101 | side_area = screen_area
102 | break
103 |
104 | elif (
105 | pos == "RIGHT"
106 | and screen_area.x == area.x + area.width + 1
107 | and screen_area.height == area.height
108 | and screen_area.y == area.y
109 | ):
110 | side_area = screen_area
111 | break
112 |
113 | elif (
114 | pos == "BOTTOM"
115 | and screen_area.width == area.width
116 | and screen_area.x == area.x
117 | and screen_area.height + screen_area.y + 1 == area.y
118 | ):
119 | side_area = screen_area
120 | break
121 |
122 | elif (
123 | pos == "LEFT"
124 | and screen_area.width + screen_area.x + 1 == area.x
125 | and screen_area.y == area.y
126 | and screen_area.height == area.height
127 | ):
128 | side_area = screen_area
129 | break
130 |
131 | return side_area
132 |
133 | def join_areas(self, context, current_area, side_area, pos, swap):
134 | context_override = ContextOverride(side_area)
135 | with context.temp_override(**context_override):
136 | bpy.ops.iops.space_data_save()
137 | bpy.ops.screen.area_close()
138 |
139 | return side_area
140 |
141 | def execute(self, context):
142 |
143 | areas = list(context.screen.areas)
144 | current_area = context.area
145 | side_area = None
146 |
147 | # Fix stupid Blender's behaviour
148 | if self.factor == 0.5:
149 | self.factor = 0.499
150 |
151 | # Check if toggle fullscreen was activated
152 | if "nonnormal" in context.window.screen.name:
153 | bpy.ops.screen.back_to_previous()
154 | return {"FINISHED"}
155 |
156 | if current_area.type == self.area_type:
157 | context_override = ContextOverride(current_area)
158 | with context.temp_override(**context_override):
159 | bpy.ops.iops.space_data_save()
160 | bpy.ops.screen.area_close()
161 | return {"FINISHED"}
162 |
163 | for area in context.screen.areas:
164 | if area == current_area:
165 | continue
166 | elif area.type == self.area_type and area.ui_type == self.ui:
167 | context_override = ContextOverride(area)
168 | with context.temp_override(**context_override):
169 | bpy.ops.iops.space_data_save()
170 | bpy.ops.screen.area_close()
171 |
172 | self.report({"INFO"}, "Joined Areas")
173 | return {"FINISHED"}
174 | else:
175 | continue
176 |
177 | if current_area.ui_type == self.ui:
178 | if self.pos == "LEFT":
179 | mirror_pos = "RIGHT"
180 | elif self.pos == "RIGHT":
181 | mirror_pos = "LEFT"
182 | elif self.pos == "TOP":
183 | mirror_pos = "BOTTOM"
184 | elif self.pos == "BOTTOM":
185 | mirror_pos = "TOP"
186 |
187 | swap = False if mirror_pos in {"TOP", "RIGHT"} else True
188 | side_area = self.get_side_area(context, current_area, mirror_pos)
189 | if side_area:
190 | self.join_areas(context, current_area, side_area, mirror_pos, swap)
191 | self.report({"INFO"}, "Joined Areas")
192 | return {"FINISHED"}
193 | else:
194 | self.report({"INFO"}, "No side area to join")
195 | return {"FINISHED"}
196 |
197 | else:
198 | new_area = None
199 | swap = True if self.factor >= 0.5 else False
200 |
201 | direction = "VERTICAL" if self.pos in {"LEFT", "RIGHT"} else "HORIZONTAL"
202 | factor = (1 - self.factor) if self.pos in {"RIGHT", "TOP"} else self.factor
203 | bpy.ops.screen.area_split(direction=direction, factor=factor)
204 |
205 | for area in context.screen.areas:
206 | if area not in areas:
207 | new_area = area
208 | break
209 |
210 | if new_area:
211 | new_area.type = self.area_type
212 | new_area.ui_type = self.ui
213 | context_override = ContextOverride(new_area)
214 | with context.temp_override(**context_override):
215 | bpy.ops.iops.space_data_load()
216 | return {"FINISHED"}
217 |
218 | return {"FINISHED"}
219 |
--------------------------------------------------------------------------------
/operators/ui_prop_switch.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | class IOPS_OT_ActiveObject_Scroll_UP(bpy.types.Operator):
5 | """Cyclic switching properties types UP"""
6 |
7 | bl_idname = "iops.object_active_object_scroll_up"
8 | bl_label = "Cyclic switching active object in selection UP"
9 |
10 | def execute(self, context):
11 | selected_objects = bpy.context.view_layer.objects.selected
12 | active = bpy.context.active_object
13 |
14 | for index, elem in enumerate(selected_objects):
15 | if elem == active:
16 | if index <= len(selected_objects):
17 | idx = index + 1
18 | try:
19 | bpy.context.view_layer.objects.active = selected_objects[idx]
20 | except IndexError:
21 | idx = 0
22 | bpy.context.view_layer.objects.active = selected_objects[idx]
23 | else:
24 | idx = 0
25 | bpy.context.view_layer.objects.active = selected_objects[idx]
26 | self.report({"INFO"}, "Active: " + context.active_object.name)
27 | return {"FINISHED"}
28 |
29 |
30 | class IOPS_OT_ActiveObject_Scroll_DOWN(bpy.types.Operator):
31 | """Cyclic switching snap with types UP"""
32 |
33 | bl_idname = "iops.object_active_object_scroll_down"
34 | bl_label = "Cyclic switching active object in selection DOWN"
35 |
36 | def execute(self, context):
37 | selected_objects = bpy.context.view_layer.objects.selected
38 | active = bpy.context.active_object
39 |
40 | for index, elem in enumerate(selected_objects):
41 | if elem == active:
42 | if index > 0 and index <= len(selected_objects):
43 | idx = index - 1
44 | try:
45 | bpy.context.view_layer.objects.active = selected_objects[idx]
46 | except IndexError:
47 | idx = 0
48 | bpy.context.view_layer.objects.active = selected_objects[idx]
49 | else:
50 | idx = len(selected_objects) - 1
51 | try:
52 | bpy.context.view_layer.objects.active = selected_objects[idx]
53 | except IndexError:
54 | idx = 0
55 | bpy.context.view_layer.objects.active = selected_objects[idx]
56 | self.report({"INFO"}, "Active: " + context.active_object.name)
57 |
58 | return {"FINISHED"}
59 |
--------------------------------------------------------------------------------
/prefs/addon_properties.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.types import PropertyGroup
3 | from bpy.props import (
4 | FloatProperty,
5 | FloatVectorProperty,
6 | StringProperty,
7 | )
8 |
9 | def update_exec_filter(self, context):
10 | scripts = bpy.context.scene['IOPS']['executor_scripts']
11 | filtered_scripts = [script for script in scripts if self.iops_exec_filter.lower() in script.lower()]
12 | bpy.context.scene['IOPS']['filtered_executor_scripts'] = filtered_scripts if len(filtered_scripts) > 0 else None
13 |
14 |
15 |
16 | class IOPS_AddonProperties(PropertyGroup):
17 | iops_panel_mesh_info: bpy.props.BoolProperty(
18 | name="Show mesh info", description="Show mesh info panel", default=False
19 | )
20 |
21 | iops_rotation_angle: FloatProperty(
22 | name="Angle", description="Degrees", default=90, min=0.0
23 | )
24 | iops_split_previous: StringProperty(
25 | name="iops_split_previous",
26 | default="VIEW_3D",
27 | )
28 | iops_exec_filter: StringProperty(
29 | name="Filter",
30 | default="",
31 | options={'TEXTEDIT_UPDATE'},
32 | update=update_exec_filter,
33 | )
34 |
35 |
36 | class IOPS_SceneProperties(PropertyGroup):
37 | dragsnap_point_a: FloatVectorProperty(
38 | name="DragSnap Point A",
39 | description="DragSnap Point A",
40 | default=(0.0, 0.0, 0.0),
41 | size=3,
42 | subtype="COORDINATES",
43 | )
44 |
45 | dragsnap_point_b: FloatVectorProperty(
46 | name="DragSnap Point B",
47 | description="DragSnap Point B",
48 | default=(0.0, 0.0, 0.0),
49 | size=3,
50 | subtype="COORDINATES",
51 | )
52 |
53 | iops_vertex_color: FloatVectorProperty(
54 | name="VertexColor",
55 | description="Color picker",
56 | default=(1.0, 1.0, 1.0, 1.0),
57 | min=0.0,
58 | max=1.0,
59 | subtype="COLOR",
60 | size=4,
61 | )
62 |
--------------------------------------------------------------------------------
/prefs/hotkeys_default.py:
--------------------------------------------------------------------------------
1 | # CTRL, ALT, Shift
2 | keys_default = [
3 | # IOPS F Keys
4 | ("iops.function_f1", "F1", "PRESS", False, False, False, False),
5 | ("iops.function_f2", "F2", "PRESS", False, False, False, False),
6 | ("iops.function_f3", "F3", "PRESS", False, False, False, False),
7 | ("iops.function_f4", "F4", "PRESS", False, False, False, False),
8 | ("iops.function_f5", "F5", "PRESS", False, False, False, False),
9 | ("iops.function_esc", "ESC", "PRESS", False, False, False, False),
10 | #IOPS Operators Cursor
11 | ("iops.cursor_rotate", "F19", "PRESS", True, True, True, False),
12 | # IOPS Operators Mesh
13 | ("iops.mesh_to_verts", "F1", "PRESS", False, True, False, False),
14 | ("iops.mesh_to_edges", "F2", "PRESS", False, True, False, False),
15 | ("iops.mesh_to_faces", "F3", "PRESS", False, True, False, False),
16 | ("iops.mesh_align_origin_to_normal", "F5", "PRESS", False, True, False, False),
17 | ("iops.mesh_mouseover_fill_select", "F19", "PRESS", True, True, True, False),
18 | ("iops.mesh_to_grid", "UP_ARROW", "PRESS", False, False, False, False),
19 | ("iops.mesh_cursor_bisect", "F19", "PRESS", False, False, False, False),
20 | # Zaloopok's operators
21 | ("iops.z_grow_loop", "F19", "PRESS", True, True, True, False),
22 | ("iops.z_shrink_loop", "F19", "PRESS", True, True, True, False),
23 | ("iops.z_grow_ring", "F19", "PRESS", True, True, True, False),
24 | ("iops.z_shrink_ring", "F19", "PRESS", True, True, True, False),
25 | ("iops.z_select_bounded_loop", "F19", "PRESS", True, True, True, False),
26 | ("iops.z_select_bounded_ring", "F19", "PRESS", True, True, True, False),
27 | ("iops.z_connect", "F19", "PRESS", True, True, True, False),
28 | ("iops.z_line_up_edges", "F19", "PRESS", True, True, True, False),
29 | ("iops.z_eq_edges", "F19", "PRESS", True, True, True, False),
30 | ("iops.z_put_on", "F19", "PRESS", True, True, True, False),
31 | ("iops.z_mirror", "F19", "PRESS", True, True, True, False),
32 | ("iops.z_delete_mode", "F19", "PRESS", True, True, True, False),
33 | # IOPS Operators Object
34 | ("iops.object_to_grid_from_active", "F19", "PRESS", True, True, True, False),
35 | ("iops.object_modal_three_point_rotation", "F19", "PRESS", True, True, True, False),
36 | ("iops.object_normalize", "UP_ARROW", "PRESS", False, False, False, False),
37 | ("iops.object_drag_snap", "S", "PRESS", True, True, True, False),
38 | ("iops.object_drag_snap_cursor", "F19", "PRESS", True, True, True, False),
39 | ("iops.uv_drag_snap_uv", "S", "PRESS", True, True, True, False),
40 | ("iops.object_active_object_scroll_up", "WHEELUPMOUSE", "PRESS", True, True, False, True),
41 | ("iops.object_active_object_scroll_down", "WHEELDOWNMOUSE", "PRESS", True, True, False, True),
42 | ("iops.object_rotate_mx", "LEFT_ARROW", "PRESS", False, False, True, False),
43 | ("iops.object_rotate_x", "LEFT_ARROW", "PRESS", False, False, False, False),
44 | ("iops.object_rotate_my", "DOWN_ARROW", "PRESS", False, False, True, False),
45 | ("iops.object_rotate_y", "DOWN_ARROW", "PRESS", False, False, False, False),
46 | ("iops.object_rotate_mz", "RIGHT_ARROW", "PRESS", False, False, True, False),
47 | ("iops.object_rotate_z", "RIGHT_ARROW", "PRESS", False, False, False, False),
48 | ("iops.mesh_uv_channel_hop", "F19", "PRESS", True, True, True, False),
49 | # IOPS Scripts
50 | ("iops.scripts_call_mt_executor", "X", "PRESS", True, True, True, False),
51 | ("iops.scripts_run_text", "F19", "PRESS", True, True, True, False),
52 | # IOPS UI Pies
53 | ("iops.call_pie_menu", "Q", "PRESS", True, True, True, False),
54 | ("iops.call_pie_edit", "F19", "PRESS", True, True, True, False),
55 | ("iops.call_pie_split", "S", "PRESS", True, True, True, False),
56 | # IOPS UI Panels
57 | ("iops.call_panel_tps", "BUTTON4MOUSE", "PRESS", False, False, True, False),
58 | ("iops.call_panel_data", "BUTTON4MOUSE", "PRESS", True, False, True, False),
59 | ("iops.call_panel_tm", "T", "PRESS", True, True, True, False),
60 | ]
61 |
--------------------------------------------------------------------------------
/prefs/iops_prefs.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 |
4 | def get_iops_prefs():
5 | prefs = bpy.context.preferences.addons['InteractionOps'].preferences
6 | snap_combo_dict = {}
7 |
8 | for i in range(1, 9):
9 | try:
10 | snap_combo_key = f"snap_combo_{i}"
11 | snap_combo_dict[snap_combo_key] = {
12 | "SNAP_ELEMENTS": {k: prefs[snap_combo_key]["SNAP_ELEMENTS"][k] for k in prefs[snap_combo_key]["SNAP_ELEMENTS"] if prefs[snap_combo_key]["SNAP_ELEMENTS"][k]},
13 | "TOOL_SETTINGS": {k: prefs[snap_combo_key]["TOOL_SETTINGS"][k] for k in prefs[snap_combo_key]["TOOL_SETTINGS"]},
14 | "TRANSFORMATION": prefs[snap_combo_key]["TRANSFORMATION"]
15 | }
16 | except KeyError:
17 | snap_combo_dict[snap_combo_key] = {
18 | "SNAP_ELEMENTS": {
19 | "INCREMENT": False,
20 | "VERTEX": True,
21 | "EDGE": False,
22 | "FACE": False,
23 | "VOLUME": False,
24 | "EDGE_MIDPOINT": False,
25 | "EDGE_PERPENDICULAR": False,
26 | "FACE_PROJECT": False,
27 | "FACE_NEAREST": False
28 | },
29 | "TOOL_SETTINGS": {
30 | "transform_pivot_point": "ACTIVE_ELEMENT",
31 | "snap_target": "ACTIVE",
32 | # "use_snap_grid_absolute": True,
33 | "use_snap_self": True,
34 | "use_snap_align_rotation": False,
35 | "use_snap_peel_object": True,
36 | "use_snap_backface_culling": False,
37 | "use_snap_selectable": False,
38 | "use_snap_translate": False,
39 | "use_snap_rotate": False,
40 | "use_snap_scale": False,
41 | "use_snap_to_same_target": False
42 | },
43 | "TRANSFORMATION": "GLOBAL"
44 | }
45 |
46 | iops_prefs = {
47 | "IOPS_DEBUG": {"IOPS_DEBUG": prefs.IOPS_DEBUG},
48 | "ALIGN_TO_EDGE": {"align_edge_color": list(prefs.align_edge_color)},
49 | "EXECUTOR": {
50 | "executor_column_count": prefs.executor_column_count,
51 | "executor_scripts_folder": prefs.executor_scripts_folder,
52 | "executor_name_lenght": prefs.executor_name_lenght,
53 | "executor_use_script_path_user": prefs.executor_use_script_path_user,
54 | "executor_scripts_subfolder": prefs.executor_scripts_subfolder,
55 | },
56 | "SPLIT_AREA_PIES": {
57 | f"PIE_{i}": {
58 | f"split_area_pie_{i}_factor": getattr(prefs, f"split_area_pie_{i}_factor"),
59 | f"split_area_pie_{i}_pos": getattr(prefs, f"split_area_pie_{i}_pos"),
60 | f"split_area_pie_{i}_ui": getattr(prefs, f"split_area_pie_{i}_ui")
61 | } for i in range(1, 10) if i != 5 # Exclude non-existing pie 5
62 | },
63 | "UI_TEXT": {
64 | "text_color": list(prefs.text_color),
65 | "text_color_key": list(prefs.text_color_key),
66 | "text_pos_x": prefs.text_pos_x,
67 | "text_pos_y": prefs.text_pos_y,
68 | "text_shadow_color": list(prefs.text_shadow_color),
69 | "text_shadow_pos_x": prefs.text_shadow_pos_x,
70 | "text_shadow_pos_y": prefs.text_shadow_pos_y,
71 | "text_shadow_toggle": prefs.text_shadow_toggle,
72 | "text_size": prefs.text_size
73 | },
74 | "UI_TEXT_STAT": {
75 | "iops_stat" : prefs.iops_stat,
76 | "text_color_stat": list(prefs.text_color_stat),
77 | "text_color_key_stat": list(prefs.text_color_key_stat),
78 | "text_color_error_stat": list(prefs.text_color_error_stat),
79 | "text_pos_x_stat": prefs.text_pos_x_stat,
80 | "text_pos_y_stat": prefs.text_pos_y_stat,
81 | "text_shadow_color_stat": list(prefs.text_shadow_color_stat),
82 | "text_shadow_pos_x_stat": prefs.text_shadow_pos_x_stat,
83 | "text_shadow_pos_y_stat": prefs.text_shadow_pos_y_stat,
84 | "text_shadow_toggle_stat": prefs.text_shadow_toggle_stat,
85 | "text_size_stat": prefs.text_size_stat,
86 | "text_column_offset_stat": prefs.text_column_offset_stat,
87 | "text_column_width_stat": prefs.text_column_width_stat
88 | },
89 | "VISUAL_ORIGIN": {
90 | "vo_cage_ap_color": list(prefs.vo_cage_ap_color),
91 | "vo_cage_ap_size": prefs.vo_cage_ap_size,
92 | "vo_cage_color": list(prefs.vo_cage_color),
93 | "vo_cage_p_size": prefs.vo_cage_p_size,
94 | "vo_cage_points_color": list(prefs.vo_cage_points_color),
95 | "vo_cage_line_thickness": prefs.vo_cage_line_thickness
96 | },
97 | "TEXTURE_TO_MATERIAL": {
98 | "texture_to_material_prefixes": prefs.texture_to_material_prefixes,
99 | "texture_to_material_suffixes": prefs.texture_to_material_suffixes
100 | },
101 | "SNAP_COMBOS": {
102 | "snap_combo_1": snap_combo_dict["snap_combo_1"],
103 | "snap_combo_2": snap_combo_dict["snap_combo_2"],
104 | "snap_combo_3": snap_combo_dict["snap_combo_3"],
105 | "snap_combo_4": snap_combo_dict["snap_combo_4"],
106 | "snap_combo_5": snap_combo_dict["snap_combo_5"],
107 | "snap_combo_6": snap_combo_dict["snap_combo_6"],
108 | "snap_combo_7": snap_combo_dict["snap_combo_7"],
109 | "snap_combo_8": snap_combo_dict["snap_combo_8"]
110 | },
111 | "DRAG_SNAP": {
112 | "drag_snap_line_thickness": prefs.drag_snap_line_thickness
113 | }
114 | }
115 |
116 | # # Update snap combos if exist in prefs
117 | # if getattr(prefs, "snap_combos", False):
118 | # for i in range(1, 9):
119 | # snap_combo = prefs.snap_combos[i-1]
120 | # iops_prefs[f"snap_combo_{i}"] = snap_combo
121 |
122 | return iops_prefs
--------------------------------------------------------------------------------
/prefs/iops_prefs_list.py:
--------------------------------------------------------------------------------
1 | iops_prefs_list = [
2 | "IOPS_DEBUG",
3 | "align_edge_color",
4 | "executor_column_count",
5 | "executor_scripts_folder",
6 | "split_area_pie_1_factor",
7 | "split_area_pie_1_pos",
8 | "split_area_pie_1_ui",
9 | "split_area_pie_2_factor",
10 | "split_area_pie_2_pos",
11 | "split_area_pie_2_ui",
12 | "split_area_pie_3_factor",
13 | "split_area_pie_3_pos",
14 | "split_area_pie_3_ui",
15 | "split_area_pie_4_factor",
16 | "split_area_pie_4_pos",
17 | "split_area_pie_4_ui",
18 | "split_area_pie_6_factor",
19 | "split_area_pie_6_pos",
20 | "split_area_pie_6_ui",
21 | "split_area_pie_7_factor",
22 | "split_area_pie_7_pos",
23 | "split_area_pie_7_ui",
24 | "split_area_pie_8_factor",
25 | "split_area_pie_8_pos",
26 | "split_area_pie_8_ui",
27 | "split_area_pie_9_factor",
28 | "split_area_pie_9_pos",
29 | "split_area_pie_9_ui",
30 | "text_color",
31 | "text_color_key",
32 | "text_pos_x",
33 | "text_pos_y",
34 | "text_shadow_color",
35 | "text_shadow_pos_x",
36 | "text_shadow_pos_y",
37 | "text_shadow_toggle",
38 | "text_size",
39 | "vo_cage_ap_color",
40 | "vo_cage_ap_size",
41 | "vo_cage_color",
42 | "vo_cage_p_size",
43 | "vo_cage_points_color",
44 | "vo_cage_line_thickness",
45 | "drag_snap_line_thickness",
46 | "texture_to_material_prefixes",
47 | "texture_to_material_suffixes",
48 | "switch_list_axis",
49 | "switch_list_ppoint",
50 | "switch_list_snap",
51 | "snap_combo_1",
52 | "snap_combo_2",
53 | "snap_combo_3",
54 | "snap_combo_4",
55 | "snap_combo_5",
56 | "snap_combo_6",
57 | ]
58 |
59 |
--------------------------------------------------------------------------------
/ui/iops_pie_edit.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | from bpy.types import Menu
4 |
5 |
6 | def get_text_icon(context, operator):
7 | if context.object.type == "MESH":
8 | match operator:
9 | case "f1":
10 | return "Vertex", "VERTEXSEL"
11 | case "f2":
12 | return "Edge", "EDGESEL"
13 | case "f3":
14 | return "Face", "FACESEL"
15 | case "esc":
16 | return "Esc", "EVENT_ESC"
17 | elif context.object.type == "ARMATURE":
18 | match operator:
19 | case "f1":
20 | return "Edit Mode", "EDITMODE_HLT"
21 | case "f2":
22 | return "Pose Mode", "POSE_HLT"
23 | case "f3":
24 | return "Set Parent to Bone", "BONE_DATA"
25 | case "esc":
26 | return "Esc", "EVENT_ESC"
27 | elif context.object.type == "EMPTY":
28 | match operator:
29 | case "f1":
30 | return "Open Instance Collection .blend", "FILE_BACKUP"
31 | case "f2":
32 | return "Make instance real", "OUTLINER_OB_GROUP_INSTANCE"
33 | case "f3":
34 | return "F3", "EVENT_F3"
35 | case _:
36 | return "Esc", "EVENT_ESC"
37 |
38 |
39 | class IOPS_MT_Pie_Edit_Submenu(Menu):
40 | bl_label = "IOPS_MT_Pie_Edit_Submenu"
41 |
42 | def draw(self, context):
43 | layout = self.layout
44 | layout.label(text="IOPS Modes")
45 | layout.separator()
46 | layout.operator("object.mode_set", text="Object Mode").mode = "OBJECT"
47 | layout.operator("object.mode_set", text="Edit Mode").mode = "EDIT"
48 | layout.operator("object.mode_set", text="Sculpt Mode").mode = "SCULPT"
49 | layout.operator("object.mode_set", text="Vertex Paint").mode = "VERTEX_PAINT"
50 | layout.operator("object.mode_set", text="Weight Paint").mode = "WEIGHT_PAINT"
51 |
52 |
53 | class IOPS_MT_Pie_Edit(Menu):
54 | # bl_idname = "iops.pie_menu"
55 | bl_label = "IOPS_MT_Pie_Edit"
56 |
57 | @classmethod
58 | def poll(self, context):
59 | return (
60 | context.area.type in {"VIEW_3D", "IMAGE_EDITOR"} and context.active_object
61 | )
62 |
63 | def draw(self, context):
64 | layout = self.layout
65 | pie = layout.menu_pie()
66 |
67 | if context.area.type == "VIEW_3D":
68 | # Open Linked Library Blend
69 | if (
70 | context.object.type == "EMPTY"
71 | and context.object.instance_collection
72 | and context.object.instance_type == "COLLECTION"
73 | # and context.object.instance_collection.library
74 | ):
75 | pie.separator()
76 | pie.separator()
77 |
78 | op = pie.operator("machin3.assemble_instance_collection", text="Expand Collection to Scene")
79 |
80 | if context.object.instance_collection.library:
81 | blendpath = os.path.abspath(
82 | bpy.path.abspath(
83 | context.object.instance_collection.library.filepath
84 | )
85 | )
86 | library = context.object.instance_collection.library.name
87 |
88 | op = pie.operator(
89 | "machin3.open_library_blend",
90 | text=f"Open {os.path.basename(blendpath)}",
91 | )
92 | op.blendpath = blendpath
93 | op.library = library
94 |
95 |
96 |
97 | # Curve
98 | elif context.object.type == "CURVE":
99 | # 4 - LEFT
100 | pie.separator()
101 | # 6 - RIGHT
102 | pie.separator()
103 | # 2 - BOTTOM
104 | pie.operator("iops.function_esc", text="Esc", icon="EVENT_ESC")
105 | # 8 - TOP
106 | pie.operator("iops.function_f1", text="Edit", icon="CURVE_DATA")
107 |
108 | else:
109 | # 4 - LEFT
110 | pie.operator(
111 | "iops.function_f1",
112 | text=get_text_icon(context, "f1")[0],
113 | icon=get_text_icon(context, "f1")[1],
114 | )
115 | # 6 - RIGHT
116 | pie.operator(
117 | "iops.function_f3",
118 | text=get_text_icon(context, "f3")[0],
119 | icon=get_text_icon(context, "f3")[1],
120 | )
121 | # 2 - BOTTOM
122 | pie.operator(
123 | "iops.function_esc",
124 | text=get_text_icon(context, "esc")[0],
125 | icon=get_text_icon(context, "esc")[1],
126 | )
127 | # 8 - TOP
128 | pie.operator(
129 | "iops.function_f2",
130 | text=get_text_icon(context, "f2")[0],
131 | icon=get_text_icon(context, "f2")[1],
132 | )
133 |
134 | # 7 - TOP - LEFT
135 | # pie.separator()
136 | # 9 - TOP - RIGHT
137 | # pie.separator()
138 | # 1 - BOTTOM - LEFT
139 | # pie.separator()
140 | # 3 - BOTTOM - RIGHT
141 | # pie.separator()
142 | # Additional items underneath
143 | box = pie.split()
144 | column = box.column()
145 | column.scale_y = 1.5
146 | column.scale_x = 1.5
147 |
148 | row = column.row(align=True)
149 |
150 | # r = row.row(align=True)
151 | # r.active = False if context.mode == 'PAINT_GPENCIL' else True
152 | # r.operator("machin3.surface_draw_mode", text="", icon="GREASEPENCIL")
153 |
154 | # r = row.row(align=True)
155 | # r.active = False if context.mode == 'TEXTURE_PAINT' else True
156 | # r.operator("object.mode_set", text="", icon="TPAINT_HLT").mode = 'TEXTURE_PAINT'
157 | if context.object.type in {"MESH"}:
158 | r = row.row(align=True)
159 | r.active = False if context.mode == "WEIGHT_PAINT" else True
160 | r.operator("object.mode_set", text="", icon="WPAINT_HLT").mode = (
161 | "WEIGHT_PAINT"
162 | )
163 |
164 | r = row.row(align=True)
165 | r.active = False if context.mode == "VERTEX_PAINT" else True
166 | r.operator("object.mode_set", text="", icon="VPAINT_HLT").mode = (
167 | "VERTEX_PAINT"
168 | )
169 |
170 | r = row.row(align=True)
171 | r.active = False if context.mode == "SCULPT" else True
172 | r.operator(
173 | "object.mode_set", text="", icon="SCULPTMODE_HLT"
174 | ).mode = "SCULPT"
175 |
176 | r = row.row(align=True)
177 | r.active = False if context.mode == "OBJECT" else True
178 | r.operator("object.mode_set", text="", icon="OBJECT_DATA").mode = (
179 | "OBJECT"
180 | )
181 |
182 | r = row.row(align=True)
183 | r.active = False if context.mode == "EDIT_MESH" else True
184 | r.operator("object.mode_set", text="", icon="EDITMODE_HLT").mode = (
185 | "EDIT"
186 | )
187 |
188 | elif context.area.type == "IMAGE_EDITOR":
189 | if context.tool_settings.use_uv_select_sync:
190 | # 4 - LEFT
191 | pie.operator("iops.function_f1", text="Vertex", icon="VERTEXSEL")
192 | # 6 - RIGHT
193 | pie.operator("iops.function_f3", text="Face", icon="FACESEL")
194 | # 2 - BOTTOM
195 | pie.operator("iops.function_esc", text="Esc", icon="EVENT_ESC")
196 | # 8 - TOP
197 | pie.operator("iops.function_f2", text="Edge", icon="EDGESEL")
198 | # 7 - TOP - LEFT
199 | elif not context.tool_settings.use_uv_select_sync:
200 | # 4 - LEFT
201 | pie.operator("iops.function_f1", text="Vertex", icon="VERTEXSEL")
202 | # 6 - RIGHT
203 | pie.operator("iops.function_f3", text="Face", icon="FACESEL")
204 | # 2 - BOTTOM
205 | pie.operator("iops.function_esc", text="Esc", icon="EVENT_ESC")
206 | # 8 - TOP
207 | pie.operator("iops.function_f2", text="Edge", icon="EDGESEL")
208 | # 7 - TOP - LEFT
209 | pie.separator()
210 | # 9 - TOP - RIGHT
211 | pie.operator("iops.function_f4", text="Island", icon="UV_ISLANDSEL")
212 |
213 |
214 | class IOPS_OT_Call_Pie_Edit(bpy.types.Operator):
215 | """IOPS Pie"""
216 |
217 | bl_idname = "iops.call_pie_edit"
218 | bl_label = "IOPS Pie Edit"
219 |
220 | @classmethod
221 | def poll(self, context):
222 | return (
223 | context.area.type in {"VIEW_3D", "IMAGE_EDITOR"} and context.active_object
224 | )
225 |
226 | def execute(self, context):
227 | bpy.ops.wm.call_menu_pie(name="IOPS_MT_Pie_Edit")
228 | return {"FINISHED"}
229 |
--------------------------------------------------------------------------------
/ui/iops_pie_menu.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.types import Menu
3 | from ..utils.functions import get_addon
4 |
5 |
6 | class IOPS_MT_Pie_Menu(Menu):
7 | # bl_idname = "iops.pie_menu"
8 | bl_label = "IOPS Pie"
9 |
10 | def draw(self, context):
11 | forgottentools, _, _, _ = get_addon("Forgotten Tools")
12 | optiloops, _, _, _ = get_addon("Optiloops")
13 | bmax_connector, _, _, _ = get_addon("BMAX Connector")
14 | bmoi_connector, _, _, _ = get_addon("BMOI Connector")
15 | # brush = context.tool_settings.image_paint.brush
16 |
17 | layout = self.layout
18 | pie = layout.menu_pie()
19 |
20 | # 4 - LEFT
21 | # pie.separator()
22 | # pie.operator("wm.call_menu_pie", text = "Some Other Pie 0", icon = "RIGHTARROW_THIN").name="Pie_menu"
23 | col = layout.menu_pie()
24 | box = col.column(align=True).box().column()
25 | box.label(text="IOPS")
26 | col.scale_x = 0.9
27 | col = box.column(align=True)
28 | col.prop(
29 | context.scene.IOPS,
30 | "iops_vertex_color",
31 | text="",
32 | )
33 | col.operator("iops.mesh_assign_vertex_color", text="Set Vertex Color")
34 | col = box.column(align=True)
35 | row = col.row(align=True)
36 | row.operator("iops.mesh_assign_vertex_color", text="White").fill_color_white = True
37 | row.operator("iops.mesh_assign_vertex_color", text="Grey").fill_color_grey = True
38 | row.operator("iops.mesh_assign_vertex_color", text="Black").fill_color_black = True
39 | col = box.column(align=True)
40 | col.operator("iops.mesh_assign_vertex_color_alpha", text="Set Vertex Alpha")
41 | col.separator()
42 | col.operator("iops.materials_from_textures", text="Materials from Textures")
43 | col.separator()
44 | col.operator("iops.object_replace", text="Object Replace")
45 | col.operator("iops.object_align_between_two", text="Align Between Two")
46 | col.operator("iops.mesh_quick_snap", text="Quick Snap")
47 | col.operator("iops.object_drop_it", text="Drop It!")
48 | col.operator("iops.object_kitbash_grid", text="Grid")
49 | col.separator()
50 | col.operator("iops.modifier_easy_array_caps", text="Easy Modifier - Array Caps")
51 | col.operator("iops.modifier_easy_array_curve", text="Easy Modifier - Array Curve")
52 | col.operator("iops.modifier_easy_curve", text="Easy Modifier - Curve")
53 | col.operator("iops.modifier_easy_shwarp", text="Easy Modifier - SHWARP")
54 | col.separator()
55 | col.operator("iops.assets_render_asset_thumbnail", text="Render Asset Thumbnail")
56 | col.separator()
57 | col.operator("iops.reload_libraries", text="Reload Libraries")
58 | col.operator("iops.reload_images", text="Reload Images")
59 |
60 | # 6 - RIGHT
61 | # pie.separator()
62 |
63 | other = pie.row()
64 | gap = other.column()
65 | gap.separator()
66 | gap.scale_y = 7
67 | other_menu = other.box().column()
68 | other_menu.scale_y = 1
69 | if bmax_connector:
70 | bmax_prefs = bpy.context.preferences.addons["BMAX_Connector"].preferences
71 | other_menu.label(text="BMax")
72 | if bmax_prefs.file_format == "FBX":
73 | other_menu.operator(
74 | "bmax.export", icon="EXPORT", text="Send to Maya/3dsmax"
75 | )
76 | other_menu.operator(
77 | "bmax.import", icon="IMPORT", text="Get from Maya/3dsmax"
78 | )
79 | if bmax_prefs.file_format == "USD":
80 | other_menu.operator(
81 | "bmax.export_usd", icon="EXPORT", text="Send to Maya/3dsmax"
82 | )
83 | other_menu.operator(
84 | "bmax.import_usd", icon="IMPORT", text="Get from Maya/3dsmax"
85 | )
86 | row = other_menu.row(align=True)
87 | row.prop(bmax_prefs, "export_reset_location", icon="EVENT_L", text=" ")
88 | row.prop(bmax_prefs, "export_reset_rotation", icon="EVENT_R", text=" ")
89 | row.prop(bmax_prefs, "export_reset_scale", icon="EVENT_S", text=" ")
90 | other_menu = other.box().column()
91 | if bmoi_connector:
92 | other_menu.label(text="BMoI")
93 | other_menu.operator("bmoi3d.export", icon="EXPORT", text="Send to MoI3D")
94 | other_menu.operator("bmoi3d.import", icon="IMPORT", text="Get from MoI3D")
95 |
96 | # 2 - BOTTOM
97 | wm = context.window_manager
98 | prefs = context.preferences.addons["B2RUVL"].preferences
99 | uvl = prefs.uvlayout_enable
100 | ruv = prefs.rizomuv_enable
101 | uvl_path = prefs.uvlayout_app_path
102 | ruv_path = prefs.rizomuv_app_path
103 |
104 | col = layout.menu_pie()
105 | box = col.column(align=True).box().column()
106 | box.label(text="B2RUVL")
107 | col_top = box.column(align=True)
108 | row = col_top.row(align=True)
109 | col_left = row.column(align=True)
110 | col_right = row.column(align=True)
111 | col_left.prop(wm.B2RUVL_PanelProperties, "uvMap_mode", text="")
112 | col_right.prop(wm.B2RUVL_PanelProperties, "uvMap")
113 | col_uvl = col_top.column(align=True)
114 | col_uvl.enabled = uvl is not False and len(uvl_path) != 0
115 | col_uvl.operator("b2ruvl.send_to_uvlayout")
116 | col_ruv = col_top.column(align=True)
117 | col_ruv.enabled = ruv is not False and len(ruv_path) != 0
118 | col_ruv.operator("b2ruvl.send_to_rizomuv")
119 | col_ruv.operator("b2ruvl.retake_rizomuv")
120 |
121 | # 8 - TOP
122 | if forgottentools and context.mode == "EDIT_MESH":
123 | other = pie.column()
124 | gap = other.column()
125 | gap.separator()
126 | gap.scale_y = 7
127 | other_menu = other.box().column()
128 | other_menu.scale_y = 1
129 | other_menu.label(text="ForgottenTools")
130 | other_menu.operator("forgotten.mesh_connect_spread")
131 | other_menu.operator("forgotten.mesh_grid_fill_all")
132 |
133 | other_menu.operator("forgotten.mesh_dice_faces")
134 | other_menu.operator("forgotten.mesh_hinge")
135 |
136 | other_menu.operator("mesh.forgotten_separate_duplicate")
137 | other_menu.operator(
138 | "wm.call_panel", text="Selection Sets", icon="SELECT_SET"
139 | ).name = "FORGOTTEN_PT_SelectionSetsPanel"
140 | else:
141 | pie.separator()
142 |
143 | # 7 - TOP - LEFT
144 | pie.separator()
145 | # 9 - TOP - RIGHT
146 | if optiloops and context.mode == "EDIT_MESH":
147 | pie.operator("mesh.optiloops")
148 | # pie.separator()
149 |
150 | # 1 - BOTTOM - LEFT
151 | pie.separator()
152 |
153 | # 3 - BOTTOM - RIGHT
154 | pie.separator()
155 |
156 |
157 | class IOPS_OT_Call_Pie_Menu(bpy.types.Operator):
158 | """IOPS Pie"""
159 |
160 | bl_idname = "iops.call_pie_menu"
161 | bl_label = "IOPS Pie Menu"
162 |
163 | def execute(self, context):
164 | bpy.ops.wm.call_menu_pie(name="IOPS_MT_Pie_Menu")
165 | return {"FINISHED"}
166 |
--------------------------------------------------------------------------------
/utils/split_areas_dict.py:
--------------------------------------------------------------------------------
1 | split_areas_dict = {
2 | "Empty": {
3 | "type": "EMPTY",
4 | "ui": "EMPTY",
5 | "icon": "NONE",
6 | "num": 0
7 | },
8 | "3D Viewport": {
9 | "type": "VIEW_3D",
10 | "ui": "VIEW_3D",
11 | "icon": "VIEW3D",
12 | "num": 1
13 | },
14 | "Image Editor": {
15 | "type": "IMAGE_EDITOR",
16 | "ui": "IMAGE_EDITOR",
17 | "icon": "IMAGE",
18 | "num": 2,
19 | },
20 | "UV Editor": {
21 | "type": "IMAGE_EDITOR",
22 | "ui": "UV",
23 | "icon": "UV",
24 | "num": 3
25 | },
26 | "Shader Editor": {
27 | "type": "NODE_EDITOR",
28 | "ui": "ShaderNodeTree",
29 | "icon": "NODE_MATERIAL",
30 | "num": 4,
31 | },
32 | "Compositor": {
33 | "type": "NODE_EDITOR",
34 | "ui": "CompositorNodeTree",
35 | "icon": "NODE_COMPOSITING",
36 | "num": 5,
37 | },
38 | "Texture Node Editor": {
39 | "type": "NODE_EDITOR",
40 | "ui": "TextureNodeTree",
41 | "icon": "NODE_TEXTURE",
42 | "num": 6,
43 | },
44 | "Video Sequencer": {
45 | "type": "SEQUENCE_EDITOR",
46 | "ui": "SEQUENCE_EDITOR",
47 | "icon": "SEQUENCE",
48 | "num": 7,
49 | },
50 | "Movie Clip Editor": {
51 | "type": "CLIP_EDITOR",
52 | "ui": "CLIP_EDITOR",
53 | "icon": "TRACKER",
54 | "num": 8,
55 | },
56 | "Dope Sheet": {
57 | "type": "DOPESHEET_EDITOR",
58 | "ui": "DOPESHEET",
59 | "icon": "ACTION",
60 | "num": 9,
61 | },
62 | "Timeline": {
63 | "type": "DOPESHEET_EDITOR",
64 | "ui": "TIMELINE",
65 | "icon": "TIME",
66 | "num": 10,
67 | },
68 | "Graph Editor": {
69 | "type": "GRAPH_EDITOR",
70 | "ui": "FCURVES",
71 | "icon": "GRAPH",
72 | "num": 11,
73 | },
74 | "Drivers": {
75 | "type": "GRAPH_EDITOR",
76 | "ui": "DRIVERS",
77 | "icon": "DRIVER",
78 | "num": 12,
79 | },
80 | "Nonlinear Animation": {
81 | "type": "NLA_EDITOR",
82 | "ui": "NLA_EDITOR",
83 | "icon": "NLA",
84 | "num": 13,
85 | },
86 | "Text Editor": {
87 | "type": "TEXT_EDITOR",
88 | "ui": "TEXT_EDITOR",
89 | "icon": "TEXT",
90 | "num": 14,
91 | },
92 | "Python Console": {
93 | "type": "CONSOLE",
94 | "ui": "CONSOLE",
95 | "icon": "CONSOLE",
96 | "num": 15,
97 | },
98 | "Info": {"type": "INFO",
99 | "ui": "INFO",
100 | "icon": "INFO",
101 | "num": 16},
102 | "Outliner": {
103 | "type": "OUTLINER",
104 | "ui": "OUTLINER",
105 | "icon": "OUTLINER",
106 | "num": 17,
107 | },
108 | "Properties": {
109 | "type": "PROPERTIES",
110 | "ui": "PROPERTIES",
111 | "icon": "PROPERTIES",
112 | "num": 18,
113 | },
114 | "File Browser": {
115 | "type": "FILE_BROWSER",
116 | "ui": "FILES",
117 | "icon": "FILEBROWSER",
118 | "num": 19,
119 | },
120 | "Preferences": {
121 | "type": "PREFERENCES",
122 | "ui": "PREFERENCES",
123 | "icon": "PREFERENCES",
124 | "num": 20,
125 | },
126 | "Geometry Nodes": {
127 | "type": "NODE_EDITOR",
128 | "ui": "GeometryNodeTree",
129 | "icon": "NODETREE",
130 | "num": 21,
131 | },
132 | "Spreadsheet": {
133 | "type": "SPREADSHEET",
134 | "ui": "SPREADSHEET",
135 | "icon": "SPREADSHEET",
136 | "num": 22,
137 | },
138 | "Asset Manager": {
139 | "type": "FILE_BROWSER",
140 | "ui": "ASSETS",
141 | "icon": "ASSET_MANAGER",
142 | "num": 23,
143 | },
144 | }
145 |
146 | split_areas_list = [
147 | (v["ui"], k, "", v["icon"], v["num"]) for k, v in split_areas_dict.items()
148 | ]
149 |
150 | split_areas_position_list = [
151 | ("LEFT", "LEFT", "", "", 0),
152 | ("RIGHT", "RIGHT", "", "", 1),
153 | ("TOP", "TOP", "", "", 2),
154 | ("BOTTOM", "BOTTOM", "", "", 3),
155 | ]
156 |
--------------------------------------------------------------------------------