Cleanup: line length
[blender.git] / release / scripts / modules / bpy_extras / node_shader_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 compliant>
20
21 import bpy
22 from mathutils import Color, Vector
23
24 __all__ = (
25     "PrincipledBSDFWrapper",
26 )
27
28
29 def _set_check(func):
30     from functools import wraps
31
32     @wraps(func)
33     def wrapper(self, *args, **kwargs):
34         if self.is_readonly:
35             assert(not "Trying to set value to read-only shader!")
36             return
37         return func(self, *args, **kwargs)
38     return wrapper
39
40 def rgb_to_rgba(rgb):
41     return list(rgb) + [1.0]
42
43 def rgba_to_rgb(rgba):
44     return Color((rgba[0], rgba[1], rgba[2]))
45
46
47 class ShaderWrapper():
48     """
49     Base class with minimal common ground for all types of shader interfaces we may want/need to implement.
50     """
51
52     # The two mandatory nodes any children class should support.
53     NODES_LIST = (
54         "node_out",
55
56         "_node_texcoords",
57     )
58
59     __slots__ = (
60         "is_readonly",
61         "material",
62         "_textures",
63         "_grid_locations",
64         *NODES_LIST,
65     )
66
67     _col_size = 300
68     _row_size = 300
69
70     def _grid_to_location(self, x, y, dst_node=None, ref_node=None):
71         if ref_node is not None:  # x and y are relative to this node location.
72             nx = round(ref_node.location.x / self._col_size)
73             ny = round(ref_node.location.y / self._row_size)
74             x += nx
75             y += ny
76         loc = None
77         while True:
78             loc = (x * self._col_size, y * self._row_size)
79             if loc not in self._grid_locations:
80                 break
81             loc = (x * self._col_size, (y - 1) * self._row_size)
82             if loc not in self._grid_locations:
83                 break
84             loc = (x * self._col_size, (y - 2) * self._row_size)
85             if loc not in self._grid_locations:
86                 break
87             x -= 1
88         self._grid_locations.add(loc)
89         if dst_node is not None:
90             dst_node.location = loc
91             dst_node.width = min(dst_node.width, self._col_size - 20)
92         return loc
93
94     def __init__(self, material, is_readonly=True, use_nodes=True):
95         self.is_readonly = is_readonly
96         self.material = material
97         if not is_readonly:
98             self.use_nodes = use_nodes
99         self.update()
100
101     def update(self):  # Should be re-implemented by children classes...
102         for node in self.NODES_LIST:
103             setattr(self, node, None)
104         self._textures = {}
105         self._grid_locations = set()
106
107
108     def use_nodes_get(self):
109         return self.material.use_nodes
110
111     @_set_check
112     def use_nodes_set(self, val):
113         self.material.use_nodes = val
114         self.update()
115
116     use_nodes = property(use_nodes_get, use_nodes_set)
117
118
119     def node_texcoords_get(self):
120         if not self.use_nodes:
121             return None
122         if self._node_texcoords is ...:
123             # Running only once, trying to find a valid texcoords node.
124             for n in self.material.node_tree.nodes:
125                 if n.bl_idname == 'ShaderNodeTexCoord':
126                     self._node_texcoords = n
127                     self._grid_to_location(0, 0, ref_node=n)
128                     break
129             if self._node_texcoords is ...:
130                 self._node_texcoords = None
131         if self._node_texcoords is None and not self.is_readonly:
132             tree = self.material.node_tree
133             nodes = tree.nodes
134             links = tree.links
135
136             node_texcoords = nodes.new(type='ShaderNodeTexCoord')
137             node_texcoords.label = "Texture Coords"
138             self._grid_to_location(-5, 1, dst_node=node_texcoords)
139             self._node_texcoords = node_texcoords
140         return self._node_texcoords
141
142     node_texcoords = property(node_texcoords_get)
143
144
145 class PrincipledBSDFWrapper(ShaderWrapper):
146     """
147     Hard coded shader setup, based in Principled BSDF.
148     Should cover most common cases on import, and gives a basic nodal shaders support for export.
149     Supports basic: diffuse/spec/reflect/transparency/normal, with texturing.
150     """
151     NODES_LIST = (
152         "node_out",
153         "node_principled_bsdf",
154
155         "_node_normalmap",
156         "_node_texcoords",
157     )
158
159     __slots__ = (
160         "is_readonly",
161         "material",
162         *NODES_LIST,
163     )
164
165     NODES_LIST = ShaderWrapper.NODES_LIST + NODES_LIST
166
167     def __init__(self, material, is_readonly=True, use_nodes=True):
168         super(PrincipledBSDFWrapper, self).__init__(material, is_readonly, use_nodes)
169
170
171     def update(self):
172         super(PrincipledBSDFWrapper, self).update()
173
174         if not self.use_nodes:
175             return
176
177         tree = self.material.node_tree
178
179         nodes = tree.nodes
180         links = tree.links
181
182         # --------------------------------------------------------------------
183         # Main output and shader.
184         node_out = None
185         node_principled = None
186         for n in nodes:
187             if n.bl_idname == 'ShaderNodeOutputMaterial' and n.inputs[0].is_linked:
188                 node_out = n
189                 node_principled = n.inputs[0].links[0].from_node
190             elif n.bl_idname == 'ShaderNodeBsdfPrincipled' and n.outputs[0].is_linked:
191                 node_principled = n
192                 for lnk in n.outputs[0].links:
193                     node_out = lnk.to_node
194                     if node_out.bl_idname == 'ShaderNodeOutputMaterial':
195                         break
196             if (
197                     node_out is not None and node_principled is not None and
198                     node_out.bl_idname == 'ShaderNodeOutputMaterial' and
199                     node_principled.bl_idname == 'ShaderNodeBsdfPrincipled'
200             ):
201                 break
202             node_out = node_principled = None  # Could not find a valid pair, let's try again
203
204         if node_out is not None:
205             self._grid_to_location(0, 0, ref_node=node_out)
206         elif not self.is_readonly:
207             node_out = nodes.new(type='ShaderNodeOutputMaterial')
208             node_out.label = "Material Out"
209             node_out.target = 'ALL'
210             self._grid_to_location(1, 1, dst_node=node_out)
211         self.node_out = node_out
212
213         if node_principled is not None:
214             self._grid_to_location(0, 0, ref_node=node_principled)
215         elif not self.is_readonly:
216             node_principled = nodes.new(type='ShaderNodeBsdfPrincipled')
217             node_principled.label = "Principled BSDF"
218             self._grid_to_location(0, 1, dst_node=node_principled)
219             # Link
220             links.new(node_principled.outputs["BSDF"], self.node_out.inputs["Surface"])
221         self.node_principled_bsdf = node_principled
222
223         # --------------------------------------------------------------------
224         # Normal Map, lazy initialization...
225         self._node_normalmap = ...
226
227         # --------------------------------------------------------------------
228         # Tex Coords, lazy initialization...
229         self._node_texcoords = ...
230
231
232     def node_normalmap_get(self):
233         if not self.use_nodes or self.node_principled_bsdf is None:
234             return None
235         node_principled = self.node_principled_bsdf
236         if self._node_normalmap is ...:
237             # Running only once, trying to find a valid normalmap node.
238             if node_principled.inputs["Normal"].is_linked:
239                 node_normalmap = node_principled.inputs["Normal"].links[0].from_node
240                 if node_normalmap.bl_idname == 'ShaderNodeNormalMap':
241                     self._node_normalmap = node_normalmap
242                     self._grid_to_location(0, 0, ref_node=node_normalmap)
243             if self._node_normalmap is ...:
244                 self._node_normalmap = None
245         if self._node_normalmap is None and not self.is_readonly:
246             tree = self.material.node_tree
247             nodes = tree.nodes
248             links = tree.links
249
250             node_normalmap = nodes.new(type='ShaderNodeNormalMap')
251             node_normalmap.label = "Normal/Map"
252             self._grid_to_location(-1, -2, dst_node=node_normalmap, ref_node=node_principled)
253             # Link
254             links.new(node_normalmap.outputs["Normal"], node_principled.inputs["Normal"])
255             self._node_normalmap = node_normalmap
256         return self._node_normalmap
257
258     node_normalmap = property(node_normalmap_get)
259
260
261     # --------------------------------------------------------------------
262     # Base Color.
263
264     def base_color_get(self):
265         if not self.use_nodes or self.node_principled_bsdf is None:
266             return self.material.diffuse_color
267         return rgba_to_rgb(self.node_principled_bsdf.inputs["Base Color"].default_value)
268
269     @_set_check
270     def base_color_set(self, color):
271         self.material.diffuse_color = color
272         if self.use_nodes and self.node_principled_bsdf is not None:
273             self.node_principled_bsdf.inputs["Base Color"].default_value = rgb_to_rgba(color)
274
275     base_color = property(base_color_get, base_color_set)
276
277
278     def base_color_texture_get(self):
279         if not self.use_nodes or self.node_principled_bsdf is None:
280             return None
281         return ShaderImageTextureWrapper(
282             self, self.node_principled_bsdf,
283             self.node_principled_bsdf.inputs["Base Color"],
284             grid_row_diff=1,
285         )
286
287     base_color_texture = property(base_color_texture_get)
288
289
290     # --------------------------------------------------------------------
291     # Specular.
292
293     def specular_get(self):
294         if not self.use_nodes or self.node_principled_bsdf is None:
295             return self.material.specular_intensity
296         return self.node_principled_bsdf.inputs["Specular"].default_value
297
298     @_set_check
299     def specular_set(self, value):
300         self.material.specular_intensity = value
301         if self.use_nodes and self.node_principled_bsdf is not None:
302             self.node_principled_bsdf.inputs["Specular"].default_value = value
303
304     specular = property(specular_get, specular_set)
305
306
307     def specular_tint_get(self):
308         if not self.use_nodes or self.node_principled_bsdf is None:
309             return 0.0
310         return self.node_principled_bsdf.inputs["Specular Tint"].default_value
311
312     @_set_check
313     def specular_tint_set(self, value):
314         if self.use_nodes and self.node_principled_bsdf is not None:
315             self.node_principled_bsdf.inputs["Specular Tint"].default_value = value
316
317     specular_tint = property(specular_tint_get, specular_tint_set)
318
319
320     # Will only be used as gray-scale one...
321     def specular_texture_get(self):
322         if not self.use_nodes or self.node_principled_bsdf is None:
323             print("NO NODES!")
324             return None
325         return ShaderImageTextureWrapper(
326             self, self.node_principled_bsdf,
327             self.node_principled_bsdf.inputs["Specular"],
328             grid_row_diff=0,
329         )
330
331     specular_texture = property(specular_texture_get)
332
333
334     # --------------------------------------------------------------------
335     # Roughness (also sort of inverse of specular hardness...).
336
337     def roughness_get(self):
338         if not self.use_nodes or self.node_principled_bsdf is None:
339             return self.material.roughness
340         return self.node_principled_bsdf.inputs["Roughness"].default_value
341
342     @_set_check
343     def roughness_set(self, value):
344         self.material.roughness = value
345         if self.use_nodes and self.node_principled_bsdf is not None:
346             self.node_principled_bsdf.inputs["Roughness"].default_value = value
347
348     roughness = property(roughness_get, roughness_set)
349
350
351     # Will only be used as gray-scale one...
352     def roughness_texture_get(self):
353         if not self.use_nodes or self.node_principled_bsdf is None:
354             return None
355         return ShaderImageTextureWrapper(
356             self, self.node_principled_bsdf,
357             self.node_principled_bsdf.inputs["Roughness"],
358             grid_row_diff=0,
359         )
360
361     roughness_texture = property(roughness_texture_get)
362
363
364     # --------------------------------------------------------------------
365     # Metallic (a.k.a reflection, mirror).
366
367     def metallic_get(self):
368         if not self.use_nodes or self.node_principled_bsdf is None:
369             return self.material.metallic
370         return self.node_principled_bsdf.inputs["Metallic"].default_value
371
372     @_set_check
373     def metallic_set(self, value):
374         self.material.metallic = value
375         if self.use_nodes and self.node_principled_bsdf is not None:
376             self.node_principled_bsdf.inputs["Metallic"].default_value = value
377
378     metallic = property(metallic_get, metallic_set)
379
380
381     # Will only be used as gray-scale one...
382     def metallic_texture_get(self):
383         if not self.use_nodes or self.node_principled_bsdf is None:
384             return None
385         return ShaderImageTextureWrapper(
386             self, self.node_principled_bsdf,
387             self.node_principled_bsdf.inputs["Metallic"],
388             grid_row_diff=0,
389         )
390
391     metallic_texture = property(metallic_texture_get)
392
393
394     # --------------------------------------------------------------------
395     # Transparency settings.
396
397     def ior_get(self):
398         if not self.use_nodes or self.node_principled_bsdf is None:
399             return 1.0
400         return self.node_principled_bsdf.inputs["IOR"].default_value
401
402     @_set_check
403     def ior_set(self, value):
404         if self.use_nodes and self.node_principled_bsdf is not None:
405             self.node_principled_bsdf.inputs["IOR"].default_value = value
406
407     ior = property(ior_get, ior_set)
408
409
410     # Will only be used as gray-scale one...
411     def ior_texture_get(self):
412         if not self.use_nodes or self.node_principled_bsdf is None:
413             return None
414         return ShaderImageTextureWrapper(
415             self, self.node_principled_bsdf,
416             self.node_principled_bsdf.inputs["IOR"],
417             grid_row_diff=-1,
418         )
419
420     ior_texture = property(ior_texture_get)
421
422
423     def transmission_get(self):
424         if not self.use_nodes or self.node_principled_bsdf is None:
425             return 0.0
426         return self.node_principled_bsdf.inputs["Transmission"].default_value
427
428     @_set_check
429     def transmission_set(self, value):
430         if self.use_nodes and self.node_principled_bsdf is not None:
431             self.node_principled_bsdf.inputs["Transmission"].default_value = value
432
433     transmission = property(transmission_get, transmission_set)
434
435
436     # Will only be used as gray-scale one...
437     def transmission_texture_get(self):
438         if not self.use_nodes or self.node_principled_bsdf is None:
439             return None
440         return ShaderImageTextureWrapper(
441             self, self.node_principled_bsdf,
442             self.node_principled_bsdf.inputs["Transmission"],
443             grid_row_diff=-1,
444         )
445
446     transmission_texture = property(transmission_texture_get)
447
448
449     # TODO: Do we need more complex handling for alpha (allowing masking and such)?
450     #       Would need extra mixing nodes onto Base Color maybe, or even its own shading chain...
451
452     # --------------------------------------------------------------------
453     # Normal map.
454
455     def normalmap_strength_get(self):
456         if not self.use_nodes or self.node_normalmap is None:
457             return 0.0
458         return self.node_normalmap.inputs["Strength"].default_value
459
460     @_set_check
461     def normalmap_strength_set(self, value):
462         if self.use_nodes and self.node_normalmap is not None:
463             self.node_normalmap.inputs["Strength"].default_value = value
464
465     normalmap_strength = property(normalmap_strength_get, normalmap_strength_set)
466
467
468     def normalmap_texture_get(self):
469         if not self.use_nodes or self.node_normalmap is None:
470             return None
471         return ShaderImageTextureWrapper(
472             self, self.node_normalmap,
473             self.node_normalmap.inputs["Color"],
474             grid_row_diff=-2,
475         )
476
477     normalmap_texture = property(normalmap_texture_get)
478
479 class ShaderImageTextureWrapper():
480     """
481     Generic 'image texture'-like wrapper, handling image node, some mapping (texture coordinates transformations),
482     and texture coordinates source.
483     """
484
485     # Note: this class assumes we are using nodes, otherwise it should never be used...
486
487     NODES_LIST = (
488         "node_dst",
489         "socket_dst",
490
491         "_node_image",
492         "_node_mapping",
493     )
494
495     __slots__ = (
496         "owner_shader",
497         "is_readonly",
498         "grid_row_diff",
499         "use_alpha",
500         *NODES_LIST,
501     )
502
503     def __new__(cls, owner_shader: ShaderWrapper, node_dst, socket_dst, *args, **kwargs):
504         instance = owner_shader._textures.get((node_dst, socket_dst), None)
505         if instance is not None:
506             return instance
507         instance = super(ShaderImageTextureWrapper, cls).__new__(cls)
508         owner_shader._textures[(node_dst, socket_dst)] = instance
509         return instance
510
511     def __init__(self, owner_shader: ShaderWrapper, node_dst, socket_dst, grid_row_diff=0, use_alpha=False):
512         self.owner_shader = owner_shader
513         self.is_readonly = owner_shader.is_readonly
514         self.node_dst = node_dst
515         self.socket_dst = socket_dst
516         self.grid_row_diff = grid_row_diff
517         self.use_alpha = use_alpha
518
519         self._node_image = ...
520         self._node_mapping = ...
521
522         tree = node_dst.id_data
523         nodes = tree.nodes
524         links = tree.links
525
526         if socket_dst.is_linked:
527             from_node = socket_dst.links[0].from_node
528             if from_node.bl_idname == 'ShaderNodeTexImage':
529                 self._node_image = from_node
530
531         if self.node_image is not None:
532             socket_dst = self.node_image.inputs["Vector"]
533             if socket_dst.is_linked:
534                 from_node = socket_dst.links[0].from_node
535                 if from_node.bl_idname == 'ShaderNodeMapping':
536                     self._node_mapping = from_node
537
538
539     def copy_from(self, tex):
540         # Avoid generating any node in source texture.
541         is_readonly_back = tex.is_readonly
542         tex.is_readonly = True
543
544         if tex.node_image is not None:
545             self.image = tex.image
546             self.projection = tex.projection
547             self.texcoords = tex.texcoords
548             self.copy_mapping_from(tex)
549
550         tex.is_readonly = is_readonly_back
551
552
553     def copy_mapping_from(self, tex):
554         # Avoid generating any node in source texture.
555         is_readonly_back = tex.is_readonly
556         tex.is_readonly = True
557
558         if tex.node_mapping is None:  # Used to actually remove mapping node.
559             if self.has_mapping_node():
560                 # We assume node_image can never be None in that case...
561                 # Find potential existing link into image's Vector input.
562                 socket_dst = socket_src = None
563                 if self.node_mapping.inputs["Vector"].is_linked:
564                     socket_dst = self.node_image.inputs["Vector"]
565                     socket_src = self.node_mapping.inputs["Vector"].links[0].from_socket
566
567                 tree = self.owner_shader.material.node_tree
568                 tree.nodes.remove(self.node_mapping)
569                 self._node_mapping = None
570
571                 # If previously existing, re-link texcoords -> image
572                 if socket_src is not None:
573                     tree.links.new(socket_src, socket_dst)
574         elif self.node_mapping is not None:
575             self.translation = tex.translation
576             self.rotation = tex.rotation
577             self.scale = tex.scale
578             self.use_min = tex.use_min
579             self.use_max = tex.use_max
580             self.min = tex.min
581             self.max = tex.max
582
583         tex.is_readonly = is_readonly_back
584
585
586     # --------------------------------------------------------------------
587     # Image.
588
589     def node_image_get(self):
590         if self._node_image is ...:
591             # Running only once, trying to find a valid image node.
592             if self.socket_dst.is_linked:
593                 node_image = self.socket_dst.links[0].from_node
594                 if node_image.bl_idname == 'ShaderNodeTexImage':
595                     self._node_image = node_image
596                     self.owner_shader._grid_to_location(0, 0, ref_node=node_image)
597             if self._node_image is ...:
598                 self._node_image = None
599         if self._node_image is None and not self.is_readonly:
600             tree = self.owner_shader.material.node_tree
601
602             node_image = tree.nodes.new(type='ShaderNodeTexImage')
603             self.owner_shader._grid_to_location(-1, 0 + self.grid_row_diff, dst_node=node_image, ref_node=self.node_dst)
604
605             tree.links.new(node_image.outputs["Alpha" if self.use_alpha else "Color"], self.socket_dst)
606
607             self._node_image = node_image
608         return self._node_image
609
610     node_image = property(node_image_get)
611
612
613     def image_get(self):
614         return self.node_image.image if self.node_image is not None else None
615
616     @_set_check
617     def image_set(self, image):
618         self.node_image.image = image
619
620     image = property(image_get, image_set)
621
622
623     def projection_get(self):
624         return self.node_image.projection if self.node_image is not None else 'FLAT'
625
626     @_set_check
627     def projection_set(self, projection):
628         self.node_image.projection = projection
629
630     projection = property(projection_get, projection_set)
631
632
633     def texcoords_get(self):
634         if self.node_image is not None:
635             socket = (self.node_mapping if self.has_mapping_node() else self.node_image).inputs["Vector"]
636             if socket.is_linked:
637                 return socket.links[0].from_socket.name
638         return 'UV'
639
640     @_set_check
641     def texcoords_set(self, texcoords):
642         # Image texture node already defaults to UVs, no extra node needed.
643         # ONLY in case we do not have any texcoords mapping!!!
644         if texcoords == 'UV' and not self.has_mapping_node():
645             return
646         tree = self.node_image.id_data
647         links = tree.links
648         node_dst = self.node_mapping if self.has_mapping_node() else self.node_image
649         socket_src = self.owner_shader.node_texcoords.outputs[texcoords]
650         links.new(socket_src, node_dst.inputs["Vector"])
651
652     texcoords = property(texcoords_get, texcoords_set)
653
654
655     def extension_get(self):
656         return self.node_image.extension if self.node_image is not None else 'REPEAT'
657
658     @_set_check
659     def extension_set(self, extension):
660         self.node_image.extension = extension
661
662     extension = property(extension_get, extension_set)
663
664
665     # --------------------------------------------------------------------
666     # Mapping.
667
668     def has_mapping_node(self):
669         return self._node_mapping not in {None, ...}
670
671     def node_mapping_get(self):
672         if self._node_mapping is ...:
673             # Running only once, trying to find a valid mapping node.
674             if self.node_image is None:
675                 return None
676             if self.node_image.inputs["Vector"].is_linked:
677                 node_mapping = self.node_image.inputs["Vector"].links[0].from_node
678                 if node_mapping.bl_idname == 'ShaderNodeMapping':
679                     self._node_mapping = node_mapping
680                     self.owner_shader._grid_to_location(0, 0 + self.grid_row_diff, ref_node=node_mapping)
681             if self._node_mapping is ...:
682                 self._node_mapping = None
683         if self._node_mapping is None and not self.is_readonly:
684             # Find potential existing link into image's Vector input.
685             socket_dst = self.node_image.inputs["Vector"]
686             # If not already existing, we need to create texcoords -> mapping link (from UV).
687             socket_src = (socket_dst.links[0].from_socket if socket_dst.is_linked
688                                                           else self.owner_shader.node_texcoords.outputs['UV'])
689
690             tree = self.owner_shader.material.node_tree
691             node_mapping = tree.nodes.new(type='ShaderNodeMapping')
692             node_mapping.vector_type = 'TEXTURE'
693             self.owner_shader._grid_to_location(-1, 0, dst_node=node_mapping, ref_node=self.node_image)
694
695             # Link mapping -> image node.
696             tree.links.new(node_mapping.outputs["Vector"], socket_dst)
697             # Link texcoords -> mapping.
698             tree.links.new(socket_src, node_mapping.inputs["Vector"])
699
700             self._node_mapping = node_mapping
701         return self._node_mapping
702
703     node_mapping = property(node_mapping_get)
704
705
706     def translation_get(self):
707         return self.node_mapping.translation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
708
709     @_set_check
710     def translation_set(self, translation):
711         self.node_mapping.translation = translation
712
713     translation = property(translation_get, translation_set)
714
715
716     def rotation_get(self):
717         return self.node_mapping.rotation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
718
719     @_set_check
720     def rotation_set(self, rotation):
721         self.node_mapping.rotation = rotation
722
723     rotation = property(rotation_get, rotation_set)
724
725
726     def scale_get(self):
727         return self.node_mapping.scale if self.node_mapping is not None else Vector((1.0, 1.0, 1.0))
728
729     @_set_check
730     def scale_set(self, scale):
731         self.node_mapping.scale = scale
732
733     scale = property(scale_get, scale_set)
734
735
736     def use_min_get(self):
737         return self.node_mapping.use_min if self_mapping.node is not None else False
738
739     @_set_check
740     def use_min_set(self, use_min):
741         self.node_mapping.use_min = use_min
742
743     use_min = property(use_min_get, use_min_set)
744
745
746     def use_max_get(self):
747         return self.node_mapping.use_max if self_mapping.node is not None else False
748
749     @_set_check
750     def use_max_set(self, use_max):
751         self.node_mapping.use_max = use_max
752
753     use_max = property(use_max_get, use_max_set)
754
755
756     def min_get(self):
757         return self.node_mapping.min if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
758
759     @_set_check
760     def min_set(self, min):
761         self.node_mapping.min = min
762
763     min = property(min_get, min_set)
764
765
766     def max_get(self):
767         return self.node_mapping.max if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
768
769     @_set_check
770     def max_set(self, max):
771         self.node_mapping.max = max
772
773     max = property(max_get, max_set)