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