Cleanup: unused imports
[blender.git] / release / scripts / modules / bpy_extras / object_utils.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (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, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8-80 compliant>
20
21 __all__ = (
22     "add_object_align_init",
23     "object_data_add",
24     "AddObjectHelper",
25     "object_add_grid_scale",
26     "object_add_grid_scale_apply_operator",
27     "object_image_guess",
28     "world_to_camera_view",
29 )
30
31
32 import bpy
33
34 from bpy.props import (
35     BoolProperty,
36     FloatVectorProperty,
37 )
38
39
40 def add_object_align_init(context, operator):
41     """
42     Return a matrix using the operator settings and view context.
43
44     :arg context: The context to use.
45     :type context: :class:`bpy.types.Context`
46     :arg operator: The operator, checked for location and rotation properties.
47     :type operator: :class:`bpy.types.Operator`
48     :return: the matrix from the context and settings.
49     :rtype: :class:`mathutils.Matrix`
50     """
51
52     from mathutils import Matrix, Vector, Euler
53     properties = operator.properties if operator is not None else None
54
55     space_data = context.space_data
56     if space_data and space_data.type != 'VIEW_3D':
57         space_data = None
58
59     # location
60     if operator and properties.is_property_set("location"):
61         location = Matrix.Translation(Vector(properties.location))
62     else:
63         location = Matrix.Translation(context.scene.cursor.location)
64
65         if operator:
66             properties.location = location.to_translation()
67
68     # rotation
69     view_align = (context.preferences.edit.object_align == 'VIEW')
70     view_align_force = False
71     if operator:
72         if properties.is_property_set("view_align"):
73             view_align = view_align_force = operator.view_align
74         else:
75             if properties.is_property_set("rotation"):
76                 # ugh, 'view_align' callback resets
77                 value = properties.rotation[:]
78                 properties.view_align = view_align
79                 properties.rotation = value
80                 del value
81             else:
82                 properties.view_align = view_align
83
84     if operator and (properties.is_property_set("rotation") and
85                      not view_align_force):
86
87         rotation = Euler(properties.rotation).to_matrix().to_4x4()
88     else:
89         if view_align and space_data:
90             rotation = space_data.region_3d.view_matrix.to_3x3().inverted()
91             rotation.resize_4x4()
92         else:
93             rotation = Matrix()
94
95         # set the operator properties
96         if operator:
97             properties.rotation = rotation.to_euler()
98
99     return location @ rotation
100
101
102 def object_data_add(context, obdata, operator=None, name=None):
103     """
104     Add an object using the view context and preference to initialize the
105     location, rotation and layer.
106
107     :arg context: The context to use.
108     :type context: :class:`bpy.types.Context`
109     :arg obdata: the data used for the new object.
110     :type obdata: valid object data type or None.
111     :arg operator: The operator, checked for location and rotation properties.
112     :type operator: :class:`bpy.types.Operator`
113     :arg name: Optional name
114     :type name: string
115     :return: the newly created object in the scene.
116     :rtype: :class:`bpy.types.Object`
117     """
118     scene = context.scene
119     layer = context.view_layer
120     layer_collection = context.layer_collection or layer.active_layer_collection
121     scene_collection = layer_collection.collection
122
123     for ob in layer.objects:
124         ob.select_set(False)
125
126     if name is None:
127         name = "Object" if obdata is None else obdata.name
128
129     obj_act = layer.objects.active
130     obj_new = bpy.data.objects.new(name, obdata)
131     scene_collection.objects.link(obj_new)
132     obj_new.select_set(True)
133     obj_new.matrix_world = add_object_align_init(context, operator)
134
135     space_data = context.space_data
136     if space_data.type == 'VIEW_3D':
137         if space_data.local_view:
138             obj_new.local_view_set(space_data, True)
139
140     if obj_act and obj_act.mode == 'EDIT' and obj_act.type == obj_new.type:
141         bpy.ops.mesh.select_all(action='DESELECT')
142         obj_act.select_set(True)
143         bpy.ops.object.mode_set(mode='OBJECT')
144
145         obj_act.select_set(True)
146         scene.update()  # apply location
147         # layer.objects.active = obj_new
148
149         # Match up UV layers, this is needed so adding an object with UV's
150         # doesn't create new layers when there happens to be a naming mis-match.
151         uv_new = obdata.uv_layers.active
152         if uv_new is not None:
153             uv_act = obj_act.data.uv_layers.active
154             if uv_act is not None:
155                 uv_new.name = uv_act.name
156
157         bpy.ops.object.join()  # join into the active.
158         if obdata:
159             bpy.data.meshes.remove(obdata)
160
161         bpy.ops.object.mode_set(mode='EDIT')
162     else:
163         layer.objects.active = obj_new
164         if context.preferences.edit.use_enter_edit_mode:
165             bpy.ops.object.mode_set(mode='EDIT')
166
167     return obj_new
168
169
170 class AddObjectHelper:
171     def view_align_update_callback(self, context):
172         if not self.view_align:
173             self.rotation.zero()
174
175     view_align: BoolProperty(
176         name="Align to View",
177         default=False,
178         update=view_align_update_callback,
179     )
180     location: FloatVectorProperty(
181         name="Location",
182         subtype='TRANSLATION',
183     )
184     rotation: FloatVectorProperty(
185         name="Rotation",
186         subtype='EULER',
187     )
188
189     @classmethod
190     def poll(self, context):
191         return context.scene.library is None
192
193
194 def object_add_grid_scale(context):
195     """
196     Return scale which should be applied on object
197     data to align it to grid scale
198     """
199
200     space_data = context.space_data
201
202     if space_data and space_data.type == 'VIEW_3D':
203         return space_data.overlay.grid_scale_unit
204
205     return 1.0
206
207
208 def object_add_grid_scale_apply_operator(operator, context):
209     """
210     Scale an operators distance values by the grid size.
211     """
212     grid_scale = object_add_grid_scale(context)
213
214     properties = operator.properties
215     properties_def = properties.bl_rna.properties
216     for prop_id in properties_def.keys():
217         if not properties.is_property_set(prop_id):
218             prop_def = properties_def[prop_id]
219             if prop_def.unit == 'LENGTH' and prop_def.subtype == 'DISTANCE':
220                 setattr(operator, prop_id,
221                         getattr(operator, prop_id) * grid_scale)
222
223
224 def object_image_guess(obj, bm=None):
225     """
226     Return a single image used by the object,
227     first checking the texture-faces, then the material.
228     """
229     # TODO, cycles/nodes materials
230     me = obj.data
231     if bm is None:
232         if obj.mode == 'EDIT':
233             import bmesh
234             bm = bmesh.from_edit_mesh(me)
235
236     if bm is not None:
237         tex_layer = bm.faces.layers.tex.active
238         if tex_layer is not None:
239             for f in bm.faces:
240                 image = f[tex_layer].image
241                 if image is not None:
242                     return image
243     else:
244         tex_layer = me.uv_textures.active
245         if tex_layer is not None:
246             for tf in tex_layer.data:
247                 image = tf.image
248                 if image is not None:
249                     return image
250
251     for m in obj.data.materials:
252         if m is not None:
253             # backwards so topmost are highest priority
254             for mtex in reversed(m.texture_slots):
255                 if mtex and mtex.use_map_color_diffuse:
256                     texture = mtex.texture
257                     if texture and texture.type == 'IMAGE':
258                         image = texture.image
259                         if image is not None:
260                             return image
261     return None
262
263
264 def world_to_camera_view(scene, obj, coord):
265     """
266     Returns the camera space coords for a 3d point.
267     (also known as: normalized device coordinates - NDC).
268
269     Where (0, 0) is the bottom left and (1, 1)
270     is the top right of the camera frame.
271     values outside 0-1 are also supported.
272     A negative 'z' value means the point is behind the camera.
273
274     Takes shift-x/y, lens angle and sensor size into account
275     as well as perspective/ortho projections.
276
277     :arg scene: Scene to use for frame size.
278     :type scene: :class:`bpy.types.Scene`
279     :arg obj: Camera object.
280     :type obj: :class:`bpy.types.Object`
281     :arg coord: World space location.
282     :type coord: :class:`mathutils.Vector`
283     :return: a vector where X and Y map to the view plane and
284        Z is the depth on the view axis.
285     :rtype: :class:`mathutils.Vector`
286     """
287     from mathutils import Vector
288
289     co_local = obj.matrix_world.normalized().inverted() @ coord
290     z = -co_local.z
291
292     camera = obj.data
293     frame = [-v for v in camera.view_frame(scene=scene)[:3]]
294     if camera.type != 'ORTHO':
295         if z == 0.0:
296             return Vector((0.5, 0.5, 0.0))
297         else:
298             frame = [(v / (v.z / z)) for v in frame]
299
300     min_x, max_x = frame[1].x, frame[2].x
301     min_y, max_y = frame[0].y, frame[1].y
302
303     x = (co_local.x - min_x) / (max_x - min_x)
304     y = (co_local.y - min_y) / (max_y - min_y)
305
306     return Vector((x, y, z))