In the past I needed the same functionality but was not able to get it to work.
But because I have a new faible to script and iterate with gemini, I tried again and voilĂ 30mins later it works. Let me know if it works on your end as well, if not feel free to iterate further via AI - I think right now we are going into a crazy new era to be able to create all those functionalities without the need of deep programming skills.
the scripts works on block and nested blocks, you can chose to change the block directly and affect all instances of it or to make a unique block.
best regards,
sebowim
# -*- coding: utf-8 -*-
"""
Script: Block Scale Resetter (Deep Fix)
Description:
Resets the scale factor of a Block Instance to (1,1,1) while keeping its
visual size and proportions exactly the same.
Features:
- Works on nested blocks (blocks inside blocks).
- Prevents "double scaling" distortion in nested hierarchies.
- Option A: Global Update (Changes the definition, affects all copies).
- Option B: Deep Unique Fix (Creates a completely isolated copy of the
block hierarchy before fixing, preserving the original blocks).
Usage:
Run the script, select a scaled block instance, and choose the mode.
"""
import rhinoscriptsyntax as rs
import Rhino
import scriptcontext as sc
import math
# ==============================================================================
# HELPER FUNCTIONS
# ==============================================================================
def GetScaleFromXform(xform):
""" Extracts the scale vector (lengths of basis vectors) from a transformation matrix. """
vx = Rhino.Geometry.Vector3d(xform.M00, xform.M10, xform.M20)
vy = Rhino.Geometry.Vector3d(xform.M01, xform.M11, xform.M21)
vz = Rhino.Geometry.Vector3d(xform.M02, xform.M12, xform.M22)
return [vx.Length, vy.Length, vz.Length]
def FindDefById(guid):
""" Finds an InstanceDefinition by ID safely across different Rhino versions. """
if hasattr(sc.doc.InstanceDefinitions, "FindId"):
return sc.doc.InstanceDefinitions.FindId(guid)
# Fallback for older versions
for idef in sc.doc.InstanceDefinitions:
if idef.Id == guid: return idef
return None
def GetUniqueName(base_name):
""" Generates a unique name for the new block definition (e.g., Name_Fixed_01). """
# Prevent stacking suffixes like Name_Fixed_Fixed
if "_Fixed" in base_name:
base_name = base_name.split("_Fixed")[0]
suffix = "_Fixed"
new_name = base_name + suffix
# Check if name exists, if so, append number
if not sc.doc.InstanceDefinitions.Find(new_name):
return new_name
i = 1
while True:
test_name = "{}_{:02d}".format(new_name, i)
if not sc.doc.InstanceDefinitions.Find(test_name):
return test_name
i += 1
# ==============================================================================
# PHASE 1: DEEP COPY / MAKE UNIQUE HIERARCHY
# ==============================================================================
def DuplicateDefinitionRecursive(idef, created_map):
"""
Recursively duplicates a block definition and ALL its nested block definitions.
Args:
idef: The InstanceDefinition to copy.
created_map: A dictionary {Old_ID: New_Def_Object} to track already copied
definitions (handles shared blocks correctly).
Returns:
The NEW InstanceDefinition object.
"""
# 1. Check if we already duplicated this specific definition in this session
if idef.Id in created_map:
return created_map[idef.Id]
# 2. Create a new unique name
new_name = GetUniqueName(idef.Name)
print(" > Duplicating Definition: {} -> {}".format(idef.Name, new_name))
# 3. Process and duplicate all objects inside the block
new_objs = []
new_attrs = []
existing_objects = idef.GetObjects()
for obj in existing_objects:
# Duplicate geometry and attributes
dup_geo = obj.Geometry.Duplicate()
dup_attr = obj.Attributes.Duplicate()
# --- HANDLE NESTED BLOCKS ---
if isinstance(dup_geo, Rhino.Geometry.InstanceReferenceGeometry):
old_child_id = dup_geo.ParentIdefId
old_child_def = FindDefById(old_child_id)
if old_child_def:
# RECURSION: Duplicate the child definition hierarchy too!
new_child_def = DuplicateDefinitionRecursive(old_child_def, created_map)
# Create a new instance geometry pointing to the NEW child definition
# but preserving the original position/rotation/scale of the instance.
new_iref = Rhino.Geometry.InstanceReferenceGeometry(new_child_def.Id, dup_geo.Xform)
dup_geo = new_iref
new_objs.append(dup_geo)
new_attrs.append(dup_attr)
# 4. Add the new definition to the Rhino document
# Block definitions are always defined around World Origin (0,0,0)
new_idef_index = sc.doc.InstanceDefinitions.Add(new_name, idef.Description, Rhino.Geometry.Point3d.Origin, new_objs, new_attrs)
if new_idef_index < 0:
print("Error creating definition: " + new_name)
return None
new_idef = sc.doc.InstanceDefinitions[new_idef_index]
# 5. Store in map and return
created_map[idef.Id] = new_idef
return new_idef
def MakeBlockTreeUniqueAndSwap(instance_guid):
"""
Entry point for Phase 1.
Duplicates the entire nested hierarchy of the selected block and
swaps the selected instance to use the new "root" definition.
"""
rhino_obj = sc.doc.Objects.Find(instance_guid)
if not rhino_obj: return None, None
# Try getting the definition (compatible with different RhinoCommon versions)
try:
old_idef = rhino_obj.InstanceDefinition
except:
old_idef = sc.doc.InstanceDefinitions.FindId(rhino_obj.InstanceDefinitionId)
if not old_idef: return None, None
# Map to track created definitions (Deep Copy Memory)
created_map = {}
# Start Recursion
new_main_idef = DuplicateDefinitionRecursive(old_idef, created_map)
if not new_main_idef: return None, None
# --- SWAP INSTANCE IN DOCUMENT ---
old_xform = rhino_obj.InstanceXform
old_attrs = rhino_obj.Attributes.Duplicate()
# Add new instance pointing to the new definition
new_guid = sc.doc.Objects.AddInstanceObject(new_main_idef.Index, old_xform, old_attrs)
# Delete the old instance
sc.doc.Objects.Delete(instance_guid, True)
# Select the new instance so the script continues smoothly
rs.SelectObject(new_guid)
return new_guid, new_main_idef.Name
# ==============================================================================
# PHASE 2: SCALE FIXING (The "Bake" Logic)
# ==============================================================================
def ResetInstanceScaleInPlace(geometry, current_scale):
"""
Critical Helper:
Takes a nested block instance (geometry) that has been scaled up by the parent process,
and resets its local scale factor to 1.0 (inverse scaling).
This prevents the "double scaling" effect where nested blocks explode in size.
"""
sx, sy, sz = current_scale
# Extract insertion point from matrix
xform = geometry.Xform
insert_pt = Rhino.Geometry.Point3d(xform.M03, xform.M13, xform.M23)
# Create pivot plane for scaling
plane = Rhino.Geometry.Plane.WorldXY
plane.Origin = insert_pt
# Apply inverse scale (shrinking the container to neutralize parent growth)
inv_scale = Rhino.Geometry.Transform.Scale(plane, 1.0/sx, 1.0/sy, 1.0/sz)
geometry.Transform(inv_scale)
def RecursiveBake(block_name, scale_vector, visited_ids):
"""
Recursively bakes the scale factor into the geometry of the block definition.
Logic:
1. Scale all geometry inside the definition UP.
2. If a nested block is found:
- Reset its instance scale DOWN (to keep it visual size).
- Recursively dive into its definition to fix its internal geometry.
"""
psx, psy, psz = scale_vector
idef = sc.doc.InstanceDefinitions.Find(block_name)
if not idef: return False
# Transformation matrix to scale everything UP
global_scale = Rhino.Geometry.Transform.Scale(Rhino.Geometry.Plane.WorldXY, psx, psy, psz)
new_objs = []
new_attrs = []
existing = idef.GetObjects()
for obj in existing:
dup_geo = obj.Geometry.Duplicate()
dup_attr = obj.Attributes.Duplicate()
# A. Transform geometry (Move to new position, scale up size)
dup_geo.Transform(global_scale)
# B. Handle Nested Blocks (Special Logic)
if isinstance(dup_geo, Rhino.Geometry.InstanceReferenceGeometry):
# 1. Neutralize the scale on the instance container
ResetInstanceScaleInPlace(dup_geo, [psx, psy, psz])
# 2. Recursively fix the definition of this nested block
child_id = dup_geo.ParentIdefId
if child_id not in visited_ids:
visited_ids.add(child_id)
child_def = FindDefById(child_id)
if child_def:
print(" > Fixing Scale in Nested Definition: {}".format(child_def.Name))
RecursiveBake(child_def.Name, [psx, psy, psz], visited_ids)
new_objs.append(dup_geo)
new_attrs.append(dup_attr)
# Update the definition with the modified geometry
sc.doc.InstanceDefinitions.ModifyGeometry(idef.Index, new_objs, new_attrs)
def ResetMainInstance(guid, scale):
"""
Resets the scale of the selected main instance in the Rhino document to (1,1,1).
This completes the process: Definition is big, Instance is small = Visual match.
"""
obj = sc.doc.Objects.Find(guid)
if not obj: return
sx, sy, sz = scale
xform = obj.InstanceXform
pt = Rhino.Geometry.Point3d(xform.M03, xform.M13, xform.M23)
plane = Rhino.Geometry.Plane.WorldXY
plane.Origin = pt
# Scale down
shrink = Rhino.Geometry.Transform.Scale(plane, 1.0/sx, 1.0/sy, 1.0/sz)
sc.doc.Objects.Transform(guid, shrink, True)
# ==============================================================================
# MAIN Execution
# ==============================================================================
def Main():
# 1. Select Block
guid = rs.GetObject("Select the SCALED Block Instance", rs.filter.instance)
if not guid: return
# 2. Get current Scale
obj = sc.doc.Objects.Find(guid)
s = GetScaleFromXform(obj.InstanceXform)
sx, sy, sz = s
# Tolerance check: Is it already 1.0?
if abs(sx-1)<1e-4 and abs(sy-1)<1e-4 and abs(sz-1)<1e-4:
print("Block is already at scale 1. No changes needed.")
return
# 3. ASK USER FOR MODE
items = ["Global Update (Affects ALL copies)", "Make Unique & Fix (Deep Copy / Safe Mode)"]
result = rs.ListBox(items, "Choose how to apply the fix:", "Block Scale Resetter")
if not result: return
rs.EnableRedraw(False)
try:
block_name = rs.BlockInstanceName(guid)
# --- PHASE 1: PREPARATION (Deep Copy) ---
if result == items[1]:
print("--- PHASE 1: Creating Deep Unique Hierarchy ---")
new_guid, new_name = MakeBlockTreeUniqueAndSwap(guid)
if new_guid:
guid = new_guid
block_name = new_name
# Re-fetch scale just in case (to ensure object validity)
obj = sc.doc.Objects.Find(guid)
s = GetScaleFromXform(obj.InstanceXform)
else:
print("Error creating unique hierarchy. Aborting.")
return
# --- PHASE 2: EXECUTION (Scale Fix) ---
print("--- PHASE 2: Fixing Proportions ({}) ---".format(block_name))
visited = set()
RecursiveBake(block_name, s, visited)
ResetMainInstance(guid, s)
print("--- SUCCESS: Block scale reset to 1. ---")
except Exception as e:
print("ERROR occurred: {}".format(e))
import traceback
traceback.print_exc()
finally:
rs.EnableRedraw(True)
if __name__=="__main__":
Main()