Link Scale script - Please help me with my code

Hi, I’m new to Python in Blender and I’m having a rough go of trying to get my script to function. Basically I am trying to replicate the “Link Scale” option in Blender 2.4x. If you check the Link Scale box, when you type or drag the object dimensions slider for either x, y, or z, the other 2 dimensions will keep their proportions. Anyway, I will post what I have and let me know where I’m going about it wrong. And yes I know the code is messy, I was planning on cleaning up once it actually works. lol

Thanks so much!

import bpy
from bpy.props import *
 
#
#    Store properties in the active scene
#
def initSceneProperties(scn):
    bpy.types.Scene.MyBool = BoolProperty(
        name = "Link Scale", 
        description = "Change object dims proportionally")
    scn['MyBool'] = False
 
initSceneProperties(bpy.context.scene)
 
#
#    Menu in UI region
#
class UIPanel(bpy.types.Panel):
    bl_label = "Link Scale"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
     
    def draw(self, context):
        layout = self.layout
        scn = context.scene
        layout.prop(scn, 'MyBool')


#
#   Actual script begins here
#


class LinkScale(bpy.types.Operator):
    """Link Scale when changing object dimensions"""
    bl_idname = "object.link_scale"
    bl_label = "Link Scale"


    def modal(self, context, event):    
        while bpy.context.scene.MyBool == True:
            if bpy.context.active_object.dimensions.x != obj_dim[0]:
                bpy.context.active_object.dimensions.y = bpy.context.active_object.dimensions.x / ratio_x_y
                bpy.context.active_object.dimensions.z = bpy.context.active_object.dimensions.x / ratio_x_z
            elif bpy.context.active_object.dimensions.y != obj_dim[1]:
                bpy.context.active_object.dimensions.x = bpy.context.active_object.dimensions.y * ratio_x_y
                bpy.context.active_object.dimensions.z = bpy.context.active_object.dimensions.y / ratio_y_z 
            elif bpy.context.active_object.dimensions.z != obj_dim[2]:   
                bpy.context.active_object.dimensions.x = bpy.context.active_object.dimensions.z * ratio_x_z
                bpy.context.active_object.dimensions.y = bpy.context.active_object.dimensions.z * ratio_y_z  
            
            return {'FINISHED'}
               
        return {'RUNNING_MODAL'}


    def invoke(self, context, event):
        if context.object:
            obj_dim = bpy.context.active_object.dimensions.copy()
        
            ratio_x_y = obj_dim[0] / obj_dim[1]
            ratio_x_z = obj_dim[0] / obj_dim[2]
            ratio_y_z = obj_dim[1] / obj_dim[2]


            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "No active object, could not finish")
            return {'CANCELLED'}
        
    
#    Registration
def register():  
    bpy.utils.register_class(UIPanel)  
    bpy.utils.register_class(LinkScale)  


def unregister():
    bpy.utils.unregister_class(LinkScale)
      
if __name__ == "__main__":  
    register() 
    

Hi roofoo,

I much prefer modal timer to modal.

Setting the link scale flag to true starts the operator, hitting escape or turning off the link scale flag, or having no active_object stops the timer.

Only coded for changing x dimension.

Needs code to stop or re-dimension if the object is changed, or the scale gets to 0.000.


import bpy
from bpy.props import *
 
#
#    Store properties in the active scene
#
def link_scale(self, context):
    if self.MyBool:
        bpy.ops.object.link_scale('INVOKE_DEFAULT')




def initSceneProperties():
    bpy.types.Scene.MyBool = BoolProperty(
        name = "Link Scale",
        default = False,
        update = link_scale,
        description = "Change object dims proportionally")


 
initSceneProperties()
 
#
#    Menu in UI region
#
class UIPanel(bpy.types.Panel):
    bl_label = "Link Scale"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
     
    def draw(self, context):
        layout = self.layout
        scn = context.scene
        layout.prop(scn, 'MyBool')
        col = layout.column()
        col.enabled = scn.MyBool
        col.prop(context.object, "dimensions")




#
#   Actual script begins here
#




class LinkScale(bpy.types.Operator):
    """Link Scale when changing object dimensions"""
    bl_idname = "object.link_scale"
    bl_label = "Link Scale"




    def modal(self, context, event):
         
        if not context.scene.MyBool or event.type == 'ESC' or context.active_object is None:
            self.cancel(context)
        
        obj_scale = self.obj_scale      
        if event.type == 'TIMER':
            obj = context.active_object


            if obj.scale.x != obj_scale.x:
                scale_factor = obj.scale.x / obj_scale.x
                obj.scale.y *= scale_factor
                obj.scale.z *= scale_factor
                self.obj_scale = obj.scale.copy()
            '''
            # change to like above for y and z
            elif context.active_object.dimensions.y != obj_dim[1]:
                context.active_object.dimensions.x = context.active_object.dimensions.y * ratio_x_y
                context.active_object.dimensions.z = context.active_object.dimensions.y * ratio_y_z 
            elif context.active_object.dimensions.z != obj_dim[2]:   
                context.active_object.dimensions.x = context.active_object.dimensions.z * ratio_x_z
                context.active_object.dimensions.y = context.active_object.dimensions.z * ratio_y_z  
            '''
            
               
        return {'PASS_THROUGH'}




    def invoke(self, context, event):
        if context.object:
            obj_scale = context.active_object.scale.copy()
        




            self.obj_scale = obj_scale
            wm = context.window_manager
            self._timer = wm.event_timer_add(0.05, context.window)
            wm.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "No active object, could not finish")
            return {'CANCELLED'}




    def cancel(self, context):
        context.scene.MyBool = False
        wm = context.window_manager
        wm.event_timer_remove(self._timer)
        return {'CANCELLED'}        
    
#    Registration
def register():  
    bpy.utils.register_class(UIPanel)  
    bpy.utils.register_class(LinkScale)  




def unregister():
    bpy.utils.unregister_class(LinkScale)
    
      
if __name__ == "__main__":  
    register()

Thanks! :slight_smile: It’s partly working now. However I notice if I enable the checkbox, change the dimensions, it scales correctly. Then when I uncheck the box, recheck it, the object starts growing and growing until it eventually reaches infinity. I’m thinking that when I uncheck the box, it needs to reset itself or something.

Edit: OK I am trying to reset the obj_scale variable in the cancel function, but keep getting an error…

Traceback (most recent call last):
  File "/Link_scale4.py", line 68, in modal
AttributeError: 'NoneType' object has no attribute 'x'

import bpy
from bpy.props import *
 
#
#    Store properties in the active scene
#
def link_scale(self, context):
    if self.MyBool:
        bpy.ops.object.link_scale('INVOKE_DEFAULT')








def initSceneProperties():
    bpy.types.Scene.MyBool = BoolProperty(
        name = "Link Scale",
        default = False,
        update = link_scale,
        description = "Change object dims proportionally")




 
initSceneProperties()
 
#
#    Menu in UI region
#
class UIPanel(bpy.types.Panel):
    bl_label = "Link Scale"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
     
    def draw(self, context):
        layout = self.layout
        scn = context.scene
        layout.prop(scn, 'MyBool')
        col = layout.column()
        col.enabled = scn.MyBool
        col.prop(context.object, "dimensions")








#
#   Actual script begins here
#








class LinkScale(bpy.types.Operator):
    """Link Scale when changing object dimensions"""
    bl_idname = "object.link_scale"
    bl_label = "Link Scale"








    def modal(self, context, event):
         
        if not context.scene.MyBool or event.type == 'ESC' or context.active_object is None:
            self.cancel(context)
        
        obj_scale = self.obj_scale      
        if event.type == 'TIMER':
            obj = context.active_object


            if obj.scale.x != obj_scale.x:
                scale_factor = obj.scale.x / obj_scale.x
                obj.scale.y *= scale_factor
                obj.scale.z *= scale_factor
                self.obj_scale = obj.scale.copy()
            elif obj.scale.y != obj_scale.y:
                scale_factor = obj.scale.y / obj_scale.y
                obj.scale.x *= scale_factor
                obj.scale.z *= scale_factor
                self.obj_scale = obj.scale.copy()
            elif obj.scale.z != obj_scale.z:
                scale_factor = obj.scale.z / obj_scale.z
                obj.scale.x *= scale_factor
                obj.scale.y *= scale_factor
                self.obj_scale = obj.scale.copy()                                
          
               
        return {'PASS_THROUGH'}








    def invoke(self, context, event):
        if context.object:
            obj_scale = context.active_object.scale.copy()
            self.obj_scale = obj_scale
            wm = context.window_manager
            self._timer = wm.event_timer_add(0.05, context.window)
            wm.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "No active object, could not finish")
            return {'CANCELLED'}








    def cancel(self, context):
        context.scene.MyBool = False
        self.obj_scale = None #Reset scale variable when unchecking box
        wm = context.window_manager
        wm.event_timer_remove(self._timer)
        return {'CANCELLED'}        
    
#    Registration
def register():  
    bpy.utils.register_class(UIPanel)  
    bpy.utils.register_class(LinkScale)  








def unregister():
    bpy.utils.unregister_class(LinkScale)
    
      
if __name__ == "__main__":  
    register()

Ok,

Should have had return self.cancel(context), my mistake.

Seems to have fixed it, I also put in an abs difference test on the scales. float != float can be dodgy.

Also stuck in code to stop if one of the scale factors hits zero, and also you may want to look at the scale locks and apply code accordingly to how you want linked scale to act if there are locked scales.


import bpy
from bpy.props import *
 
#
#    Store properties in the active scene
#
def link_scale(self, context):
    if self.link_object_scale:
        bpy.ops.object.link_scale('INVOKE_DEFAULT')




def initSceneProperties():
    bpy.types.Scene.link_object_scale = BoolProperty(
        name = "Link Scale",
        default = False,
        update = link_scale,
        description = "Change object dims proportionally")


 
initSceneProperties()
 
#
#    Menu in UI region
#
class UIPanel(bpy.types.Panel):
    bl_label = "Link Scale"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
     
    def draw_header(self, context):
        scn = context.scene
        self.layout.prop(scn, 'link_object_scale', text="")        
    
    def draw(self, context):
        layout = self.layout


        obj = context.object
        err = 0.0 in obj.scale
        if err:
           layout.label("ZERO Error", icon='ERROR')


        layout.label("Dimensions")
        row = layout.row(align=True)
        col = row.column()
        #col.enabled = scn.link_object_scale or not err
        col.prop(context.object, "dimensions", text="")
        col = row.column()
        col.alignment = 'RIGHT'
        col.prop(context.object, "lock_scale", text="")




#
#   Actual script begins here
#




class LinkScale(bpy.types.Operator):
    """Link Scale when changing object dimensions"""
    bl_idname = "object.link_scale"
    bl_label = "Link Scale"
    _timer = None
    link_scale = None
    
    def modal(self, context, event):
        TOL = 0.00001
        
        if not context.scene.link_object_scale or event.type == 'ESC' or context.active_object is None:
            return self.cancel(context)
        
        obj_scale = self.obj_scale      
        if event.type == 'TIMER':
            obj = context.active_object
            
            if 0.0 in obj.scale:
                print("scale factor is zero")
                return self.cancel(context)
            
            if (obj.scale - obj_scale).length < TOL:
                return {'PASS_THROUGH'}
            
            if abs(obj.scale.x - obj_scale.x) > TOL: # and not obj.lock_scale[0]:
                scale_factor = obj.scale.x / obj_scale.x
                obj.scale.y *= scale_factor
                obj.scale.z *= scale_factor
                
            elif abs(obj.scale.y - obj_scale.y) > TOL: # and not obj.lock_scale[1]:
                scale_factor = obj.scale.y / obj_scale.y
                obj.scale.x *= scale_factor
                obj.scale.z *= scale_factor
                
            elif abs(obj.scale.z - obj_scale.z) > TOL: # and not obj.lock_scale[2]:
                scale_factor = obj.scale.z / obj_scale.z
                obj.scale.x *= scale_factor
                obj.scale.y *= scale_factor
            
            self.obj_scale = obj.scale.copy()                                
          
               
        return {'PASS_THROUGH'}




    def invoke(self, context, event):
        if context.object is not None:
            obj_scale = context.active_object.scale.copy()
            self.obj_scale = obj_scale
            wm = context.window_manager
            self._timer = wm.event_timer_add(0.05, context.window)
            wm.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "No active object, could not finish")
            return {'CANCELLED'}




    def cancel(self, context):
        #
        #self.obj_scale = None #Reset scale variable when unchecking box
        context.scene.link_object_scale = False
        wm = context.window_manager
        wm.event_timer_remove(self._timer)
        return {'CANCELLED'}        
    
#    Registration
def register():  
    bpy.utils.register_class(UIPanel)  
    bpy.utils.register_class(LinkScale)  




def unregister():
    bpy.utils.unregister_class(LinkScale)
    bpy.utils.unregister_class(UIPanel)
      
if __name__ == "__main__": 
    bpy.context.scene.link_object_scale = False 
    register()

Wow, thanks! This works pretty well. :yes: BTW how would I cancel the scaling if the user selects a different object? Because currently if the checkbox is on, I change dimensions, and then I select a different object, the newly selected object immediately changes size. I tried storing the name of the active object on invoke and then comparing that name to the value of the active object in the modal but it doesn’t seem to work.

Also, just curious what the TOL variable is for.
Sorry for my noob-ishness! haha

On the tolerance variable TOL,

Instead of comparing two floats with equality or non equality, an arbitrarily small tolerance is used when comparing the absolute difference.

On a different object, yeah store the name in invoke, make sure you use self.object_name, and not just object_name and compare it in the modal part. You could then either cancel the operation or reset the obj_scale


        if event.type == 'TIMER':
            obj = context.active_object
            # new bit
            if obj.name != self.object_name:
               #return self.cancel(context)  # new object cancel.
               # or reset the object_name and obj_scale
               self.object_name = obj.name
               self.obj_scale = obj.scale.copy()

Awesome, batFINGER, thanks! I’m going with the cancel if a new object is selected. I tried with the last two lines you posted but when you select the second object it still jumps in size.

And I added the code for the scale lock. If a x,y, or z dimension is locked then when the checkbox is True, it does not change. So I can lock z and change the value of x and only x and y will change. Thanks for pointing me in the right direction!

Anyways, here’s the code as it stands now, in case you or anybody else wants to play with it. And I converted it to an add-on. :eyebrowlift:

bl_info = {
    "name": "Link Scale",
    "author": "Joel Wagner, with help from batFINGER",
    "version": (0, 1),
    "blender": (2, 69, 0),
    "location": "3D View -> Properties -> Link Scale",
    "description": "Change object dimensions and keep them in proportion",
    "category": "Object"}


import bpy
from bpy.props import *
 
#
#    Store properties in the active scene
#
def link_scale(self, context):
    if self.link_object_scale:
        bpy.ops.object.link_scale('INVOKE_DEFAULT')


def initSceneProperties():
    bpy.types.Scene.link_object_scale = BoolProperty(
        name = "Link Scale",
        default = False,
        update = link_scale,
        description = "Change object dims proportionally")

 
initSceneProperties()
 
#
#    Menu in UI region
#
class UIPanel(bpy.types.Panel):
    bl_label = "Link Scale"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
     
    def draw_header(self, context):
        scn = context.scene
        self.layout.prop(scn, 'link_object_scale', text="")        
    
    def draw(self, context):
        layout = self.layout


        obj = context.object
        err = 0.0 in obj.scale
        if err:
           layout.label("ZERO Error", icon='ERROR')


        layout.label("Dimensions")
        row = layout.row(align=True)
        col = row.column()
        col.prop(context.object, "dimensions", text="")
        col = row.column()
        col.alignment = 'RIGHT'
        col.prop(context.object, "lock_scale", text="")




#
#   Actual script begins here
#


class LinkScale(bpy.types.Operator):
    """Link Scale when changing object dimensions"""
    bl_idname = "object.link_scale"
    bl_label = "Link Scale"
    _timer = None
    link_scale = None
    
    def modal(self, context, event):
        TOL = 0.00001
        
        if not context.scene.link_object_scale or event.type == 'ESC' or context.active_object is None:
            return self.cancel(context)
        
        obj_scale = self.obj_scale      
        if event.type == 'TIMER':
            obj = context.active_object
            if obj.name != self.object_name or not bpy.data.objects[self.object_name].select:
               return self.cancel(context)  # new object cancel.
               # or reset the object_name and obj_scale
               #self.object_name = obj.name
               #self.obj_scale = obj.scale.copy()
               
            if 0.0 in obj.scale:
                print("scale factor is zero")
                return self.cancel(context)
            
            if (obj.scale - obj_scale).length < TOL:
                return {'PASS_THROUGH'}
            
            if abs(obj.scale.x - obj_scale.x) > TOL and not obj.lock_scale[0]:
                scale_factor = obj.scale.x / obj_scale.x
                if not obj.lock_scale[1]:
                    obj.scale.y *= scale_factor
                if not obj.lock_scale[2]:    
                    obj.scale.z *= scale_factor
                
            elif abs(obj.scale.y - obj_scale.y) > TOL and not obj.lock_scale[1]:
                scale_factor = obj.scale.y / obj_scale.y
                if not obj.lock_scale[0]:                
                    obj.scale.x *= scale_factor
                if not obj.lock_scale[2]:                    
                    obj.scale.z *= scale_factor
                
            elif abs(obj.scale.z - obj_scale.z) > TOL and not obj.lock_scale[2]:
                scale_factor = obj.scale.z / obj_scale.z
                if not obj.lock_scale[0]:                
                    obj.scale.x *= scale_factor
                if not obj.lock_scale[1]:                    
                    obj.scale.y *= scale_factor
            
            self.obj_scale = obj.scale.copy()                                
          
               
        return {'PASS_THROUGH'}




    def invoke(self, context, event):
        if context.object is not None:
            obj_scale = context.active_object.scale.copy()
            self.obj_scale = obj_scale
            object_name = context.active_object.name
            self.object_name = object_name
            wm = context.window_manager
            self._timer = wm.event_timer_add(0.05, context.window)
            wm.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "No active object, could not finish")
            return {'CANCELLED'}




    def cancel(self, context):

        context.scene.link_object_scale = False
        wm = context.window_manager
        wm.event_timer_remove(self._timer)
        return {'CANCELLED'}        
    
#    Registration
def register():  
    bpy.utils.register_class(UIPanel)  
    bpy.utils.register_class(LinkScale)  



def unregister():
    bpy.utils.unregister_class(LinkScale)
    bpy.utils.unregister_class(UIPanel)
      
if __name__ == "__main__": 
    bpy.context.scene.link_object_scale = False 
    register()

Attachments

link_scale_0_1.py.zip (1.59 KB)

Works well,

The object hassle was


            if obj.name != self.object_name: # or not bpy.data.objects[self.object_name].select:
               #return self.cancel(context)  # new object cancel.
               # or reset the object_name and obj_scale
               self.object_name = obj.name
               obj_scale = self.obj_scale = obj.scale.copy()

my lazy fault again, when I changed code I put in obj_scale = self.obj_scale rather than replace all with self.ob_scale. Anyway, it would have caused the changed object to scale based on the previous ones scale vector.

Have you considered extending this to a selected_objects level?

Might need to investigate how to handle the compatibility issue with the resize operator.

Thanks again for your help batFINGER! :slight_smile:

I’m not sure what you mean by extending this to a selected_objects level.

And yes I see what you mean about scaling in the viewport with this turned on. It acts funny. I guess it needs to temporarily disable the Link Scale when the user changes the scale using the S key. Any way to determine when scale mode is activated?