Python
"""
Cinema 4D - Complete Polygon Unfold System
Creates an animated unfolding effect from polygon selections
Updated for Cinema 4D 2025-2026
NOW WITH UVW PRESERVATION
"""
import c4d
from c4d import gui
import math
import re
# ============================================================================
# UNFOLD CONTROLLER TAG CODE
# ============================================================================
UNFOLD_TAG_CODE = """\"\"\"
Cinema 4D Python Tag - Range Map Script
Animates child objects with progressive rotation and visibility control
\"\"\"
import c4d
import math
def range_map(value, min_in, max_in, min_out, max_out, limit=True):
\"\"\"
Maps a value from one range to another with optional clamping.
Args:
value: Input value to map
min_in: Minimum of input range
max_in: Maximum of input range
min_out: Minimum of output range
max_out: Maximum of output range
limit: Whether to clamp the output to the output range
Returns:
Mapped value
\"\"\"
# Avoid division by zero
if max_in == min_in:
return min_out
# Normalize and remap
normalized = (value - min_in) / (max_in - min_in)
output = normalized * (max_out - min_out) + min_out
# Apply limiting if enabled
if limit:
if max_out < min_out:
output = max(max_out, min(min_out, output))
else:
output = max(min_out, min(max_out, output))
return output
def get_child_objects(parent):
\"\"\"
Gets all direct children of a parent object.
This matches the original logic: GetDown() for first child,
then traverse down the hierarchy.
Args:
parent: Parent object to get children from
Returns:
List of child objects
\"\"\"
children = []
child = parent.GetDown()
while child is not None:
children.append(child)
child = child.GetDown()
return children
def get_rotation_axis(axis_index, rotation_vector):
\"\"\"
Gets rotation value for specified axis.
Args:
axis_index: 0=Y, 1=X, 2=Z
rotation_vector: c4d.Vector containing rotation
Returns:
Rotation value for the specified axis
\"\"\"
axis_map = {
0: rotation_vector.y, # Y axis
1: rotation_vector.x, # X axis
2: rotation_vector.z # Z axis
}
return axis_map.get(axis_index, 0)
def set_rotation_axis(axis_index, rotation_value):
\"\"\"
Creates a rotation vector for specified axis.
Args:
axis_index: 0=Y, 1=X, 2=Z
rotation_value: Rotation value to set
Returns:
c4d.Vector with rotation on specified axis
\"\"\"
axis_map = {
0: c4d.Vector(0, rotation_value, 0), # Y axis
1: c4d.Vector(rotation_value, 0, 0), # X axis
2: c4d.Vector(0, 0, rotation_value) # Z axis
}
return axis_map.get(axis_index, c4d.Vector(0, 0, 0))
def set_object_visibility(obj, visible):
\"\"\"
Sets object visibility in editor and renderer.
Args:
obj: Object to modify
visible: True for visible (0), False for hidden/greyed (1)
\"\"\"
# 0 = visible, 1 = greyed out/hidden in editor but still rendered
visibility_value = 0 if visible else 1
obj[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = visibility_value
obj[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = visibility_value
def main():
\"\"\"Main execution function for the Cinema 4D Python tag.\"\"\"
# Get user data parameters from the tag (op)
use_effect = op[c4d.ID_USERDATA, 1]
unfold_amount = op[c4d.ID_USERDATA, 2]
unfold_fade = op[c4d.ID_USERDATA, 3]
rotation_axis = op[c4d.ID_USERDATA, 4]
clockwise = op[c4d.ID_USERDATA, 5]
invert_order = op[c4d.ID_USERDATA, 6]
# Get all child objects from the object this tag is attached to
children = get_child_objects(op.GetObject())
# If effect is disabled, hide all children and exit
if not use_effect:
for child in children:
child[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 2
child[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 2
return False
# Reverse order if inverted
if not invert_order:
children = children[::-1]
# Update object count in user data
object_count = len(children)
op[c4d.ID_USERDATA, 7] = object_count
# Handle empty children list
if object_count == 0:
return False
# Determine rotation direction
pi_value = math.pi if clockwise else -math.pi
# Process each child object
for index, child in enumerate(children):
# Calculate input range for this object
range_size = 1.0 / object_count
if index == 0:
min_in = range_size * index
else:
# Apply fade factor to create overlap
min_in = (range_size * index) - (range_size * index * unfold_fade)
max_in = range_size * (index + 1)
# Calculate output range (rotation)
min_out = 0.0
frozen_rotation = child.GetFrozenRot()
max_out = pi_value - get_rotation_axis(rotation_axis, frozen_rotation)
# Map unfold amount to rotation
rotation = range_map(unfold_amount, min_in, max_in, min_out, max_out, limit=True)
# Set visibility based on unfold progress
# Visible (0) while animating, greyed out (1) after animation completes
if unfold_amount <= max_in:
child[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 0
child[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 0
else:
child[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 1
child[c4d.ID_BASEOBJECT_VISIBILITY_RENDER] = 1
# Apply rotation
rotation_vector = set_rotation_axis(rotation_axis, rotation)
child.SetRelRot(rotation_vector)
return False
"""
# ============================================================================
# COORDINATE TRANSFORMATION UTILITIES
# ============================================================================
def Pos2Off(obj, local_pos):
"""Convert local position to global offset"""
if isinstance(obj, c4d.BaseObject):
mg = obj.GetMg()
elif isinstance(obj, c4d.Matrix):
mg = obj
else:
mg = obj
if isinstance(local_pos, c4d.Vector):
pos = local_pos
elif isinstance(local_pos, c4d.Matrix):
pos = local_pos.off
elif isinstance(local_pos, c4d.BaseObject):
pos = local_pos.GetMg().off
else:
pos = local_pos
return mg.Mul(pos)
def Off2Pos(obj, global_pos):
"""Convert global offset to local position"""
if isinstance(obj, c4d.BaseObject):
mg = obj.GetMg()
elif isinstance(obj, c4d.Matrix):
mg = obj
else:
mg = obj
if isinstance(global_pos, c4d.Vector):
pos = global_pos
elif isinstance(global_pos, c4d.Matrix):
pos = global_pos.off
elif isinstance(global_pos, c4d.BaseObject):
pos = global_pos.GetMg().off
else:
pos = global_pos
return mg.__invert__().Mul(pos)
def FromMgToMl(mother, child_global_pos, child_global_rot):
"""Convert global matrix to local matrix relative to mother"""
mg = mother.GetMg()
rot = child_global_rot.GetMg()
pos = child_global_pos.GetMg().off
return c4d.Matrix.__mul__(mg.__invert__(), c4d.Matrix(pos, rot.v1, rot.v2, rot.v3))
def Abs2Frozen(obj):
"""Convert absolute position/rotation to frozen coordinates"""
obj.SetFrozenPos(obj.GetAbsPos())
obj.SetFrozenRot(obj.GetAbsRot())
obj.SetRelPos(c4d.Vector(0, 0, 0))
obj.SetRelRot(c4d.Vector(0, 0, 0))
def Rel2Frozen(obj):
"""Convert relative position/rotation to frozen coordinates"""
obj.SetFrozenPos(obj.GetRelPos())
obj.SetFrozenRot(obj.GetRelRot())
obj.SetRelPos(c4d.Vector(0, 0, 0))
obj.SetRelRot(c4d.Vector(0, 0, 0))
def Frozen2Rel(obj):
"""Convert frozen coordinates to relative position/rotation"""
obj.SetRelPos(obj.GetFrozenPos())
obj.SetRelRot(obj.GetFrozenRot())
obj.SetFrozenPos(c4d.Vector(0, 0, 0))
obj.SetFrozenRot(c4d.Vector(0, 0, 0))
def GetGlobalPointsTuple(obj):
"""Get all points in global coordinates as tuple"""
points = obj.GetAllPoints()
gl_points = ()
for i in points:
gl_points = gl_points + (Pos2Off(obj, i),)
return gl_points
def GetDistance(vec1, vec2):
"""Calculate distance between two vectors"""
dist = vec1 - vec2
return math.sqrt(dist.x**2 + dist.y**2 + dist.z**2)
def AimAxis(pos, aim_pos, aim_axis, up_pos, up_axis):
"""Create matrix aligned to target position with specified axes"""
axis = (aim_axis, up_axis)
new_Ml = c4d.Matrix()
new_Ml.off = pos
# Validate and calculate aim and up vectors
for i in axis:
if re.search(r"\A[-+]{,1}[xyz]\Z", i) is None:
return None
if re.search(r"\A-[xyz]\Z", i) is not None:
if i == axis[0]:
aim_vec = -(aim_pos - pos)
if i == axis[1]:
up_vec = -(pos - up_pos)
else:
if i == axis[0]:
aim_vec = aim_pos - pos
if i == axis[1]:
up_vec = pos - up_pos
# Calculate normalized vectors
aim_vec_norm = aim_vec.GetNormalized()
third_vec_norm = aim_vec.Cross(up_vec).GetNormalized()
up_vec_norm = aim_vec.Cross(third_vec_norm).GetNormalized()
# Assign vectors to matrix axes
for i in axis:
if i == axis[0]:
if re.search(r"\A[-+]{,1}[x]\Z", i) is not None:
new_Ml.v1 = aim_vec_norm
if re.search(r"\A[-+]{,1}[y]\Z", i) is not None:
new_Ml.v2 = aim_vec_norm
if re.search(r"\A[-+]{,1}[z]\Z", i) is not None:
new_Ml.v3 = aim_vec_norm
if i == axis[1]:
if re.search(r"\A[-+]{,1}[x]\Z", i) is not None:
new_Ml.v1 = up_vec_norm
if re.search(r"\A[-+]{,1}[y]\Z", i) is not None:
new_Ml.v2 = up_vec_norm
if re.search(r"\A[-+]{,1}[z]\Z", i) is not None:
new_Ml.v3 = up_vec_norm
# Extract axis characters
new_str = ""
for i in range(len(str(axis))):
if re.search(r"[xyz]", str(axis)[i]) is not None:
new_str = new_str + str(axis)[i]
# Check for duplicate axes
if len(new_str) < 2 or new_str[0] == new_str[1]:
return None
# Calculate third axis
if new_str in ["zy", "xz", "yx"]:
if re.search(r"\A[^x]{1,2}\Z", new_str) is not None:
new_Ml.v1 = third_vec_norm
if re.search(r"\A[^y]{1,2}\Z", new_str) is not None:
new_Ml.v2 = third_vec_norm
if re.search(r"\A[^z]{1,2}\Z", new_str) is not None:
new_Ml.v3 = third_vec_norm
else:
if re.search(r"\A[^x]{1,2}\Z", new_str) is not None:
new_Ml.v1 = -third_vec_norm
if re.search(r"\A[^y]{1,2}\Z", new_str) is not None:
new_Ml.v2 = -third_vec_norm
if re.search(r"\A[^z]{1,2}\Z", new_str) is not None:
new_Ml.v3 = -third_vec_norm
return new_Ml
# ============================================================================
# UVW UTILITIES
# ============================================================================
def GetUVWTag(obj):
"""Get the first UVW tag from an object"""
tag = obj.GetTag(c4d.Tuvw)
return tag
def CopyUVWData(source_obj, source_poly_id, dest_obj, dest_poly_id=0):
"""
Copy UVW data from a specific polygon on source object
to a polygon on destination object
Args:
source_obj: Source polygon object
source_poly_id: Index of polygon on source
dest_obj: Destination polygon object
dest_poly_id: Index of polygon on destination (usually 0)
Returns:
True if successful, False otherwise
"""
# Get source UVW tag
source_uvw_tag = GetUVWTag(source_obj)
if source_uvw_tag is None:
return False
# Get or create destination UVW tag
dest_uvw_tag = GetUVWTag(dest_obj)
if dest_uvw_tag is None:
dest_uvw_tag = c4d.UVWTag(dest_obj.GetPolygonCount())
dest_obj.InsertTag(dest_uvw_tag)
# Get UVW data from source polygon
uvw_dict = source_uvw_tag.GetSlow(source_poly_id)
# Set UVW data to destination polygon
dest_uvw_tag.SetSlow(dest_poly_id, uvw_dict["a"], uvw_dict["b"], uvw_dict["c"], uvw_dict["d"])
return True
def CopyAllUVWTags(source_obj, dest_obj, source_poly_id, dest_poly_id=0):
"""
Copy UVW data from all UVW tags on source object to destination object
Args:
source_obj: Source polygon object
source_poly_id: Index of polygon on source
dest_obj: Destination polygon object
dest_poly_id: Index of polygon on destination (usually 0)
Returns:
Number of UVW tags copied
"""
copied_count = 0
# Iterate through all tags on source object
tag = source_obj.GetFirstTag()
while tag:
if tag.GetType() == c4d.Tuvw:
# Get UVW data from source polygon
uvw_dict = tag.GetSlow(source_poly_id)
# Create corresponding tag on destination
dest_uvw_tag = c4d.UVWTag(dest_obj.GetPolygonCount())
dest_uvw_tag.SetName(tag.GetName())
dest_obj.InsertTag(dest_uvw_tag)
# Set UVW data to destination polygon
dest_uvw_tag.SetSlow(dest_poly_id, uvw_dict["a"], uvw_dict["b"], uvw_dict["c"], uvw_dict["d"])
copied_count += 1
tag = tag.GetNext()
return copied_count
def CopyMaterialTags(source_obj, dest_obj):
"""
Copy all material (texture) tags from source to destination object
Args:
source_obj: Source object
dest_obj: Destination object
Returns:
Number of material tags copied
"""
copied_count = 0
# Iterate through all tags on source object
tag = source_obj.GetFirstTag()
while tag:
if tag.GetType() == c4d.Ttexture:
# Clone the material tag
new_tag = tag.GetClone()
dest_obj.InsertTag(new_tag)
copied_count += 1
tag = tag.GetNext()
return copied_count
# ============================================================================
# POLYGON SPLITTING
# ============================================================================
def GetSelectedPolygons(obj):
"""Get selected polygons with their indices"""
polys = obj.GetAllPolygons()
sel = obj.GetPolygonS().GetAll(obj.GetPolygonCount())
selection = []
for i in range(len(sel)):
if sel[i] == 1:
selection.append({"id": i, "poly": polys[i]})
return selection if len(selection) > 0 else None
def SplitPolygons(obj, axis_mode="smooth"):
"""
Split selected polygons into individual objects
axis_mode: 'smooth', 'straight', 'diagonal_a', 'diagonal_b'
"""
sel = GetSelectedPolygons(obj)
if sel is None:
return None
# Create container object
container = c4d.BaseObject(c4d.Onull)
container.SetName(obj.GetName() + "_Unfold")
container.SetMg(obj.GetMg())
doc.InsertObject(container, obj.GetUp(), obj)
# Create new object without selected polygons
all_poly = obj.GetAllPolygons()
sel_id_set = set([s["id"] for s in sel])
new_poly_list = [all_poly[i] for i in range(len(all_poly)) if i not in sel_id_set]
new_obj = c4d.BaseObject(c4d.Opolygon)
new_obj.SetName(obj.GetName() + "_Base")
new_obj.SetMg(obj.GetMg())
new_obj.ResizeObject(obj.GetPointCount(), len(new_poly_list))
for i in range(obj.GetPointCount()):
new_obj.SetPoint(i, obj.GetPoint(i))
for i in range(len(new_poly_list)):
new_obj.SetPolygon(i, new_poly_list[i])
doc.InsertObject(new_obj, container, None)
# Copy UVW tags to base object (for non-selected polygons)
has_uvw = False
source_uvw_tag = GetUVWTag(obj)
if source_uvw_tag is not None:
has_uvw = True
# Create UVW tag on base object
dest_uvw_tag = c4d.UVWTag(new_obj.GetPolygonCount())
new_obj.InsertTag(dest_uvw_tag)
# Copy UVW data for all non-selected polygons
dest_poly_idx = 0
for i in range(len(all_poly)):
if i not in sel_id_set:
uvw_dict = source_uvw_tag.GetSlow(i)
dest_uvw_tag.SetSlow(dest_poly_idx, uvw_dict["a"], uvw_dict["b"], uvw_dict["c"], uvw_dict["d"])
dest_poly_idx += 1
# Copy material tags to base object
CopyMaterialTags(obj, new_obj)
# Create individual polygon objects
poly_objects = []
for r_poly in sel:
cp = r_poly["poly"]
poly_id = r_poly["id"]
# Detect triangle or quad
is_quad = cp.c != cp.d
if is_quad:
points = {"a": cp.a, "b": cp.b, "c": cp.c, "d": cp.d}
pcnt = 4
else:
points = {"a": cp.a, "b": cp.b, "c": cp.c}
pcnt = 3
# Calculate center
centre = c4d.Vector(0, 0, 0)
for key in points:
centre = centre + obj.GetPoint(points[key])
centre = centre / len(points)
# Calculate alignment based on mode
if axis_mode == "smooth":
if is_quad:
bc = (obj.GetPoint(points["b"]) + obj.GetPoint(points["c"])) / 2
ad = (obj.GetPoint(points["a"]) + obj.GetPoint(points["d"])) / 2
align = (bc - ad) + centre
ab = (obj.GetPoint(points["a"]) + obj.GetPoint(points["b"])) / 2
cd = (obj.GetPoint(points["c"]) + obj.GetPoint(points["d"])) / 2
up = (ab - cd) + centre
else:
align = obj.GetPoint(points["c"])
up = (obj.GetPoint(points["a"]) + obj.GetPoint(points["b"])) / 2
elif axis_mode == "straight":
align = (obj.GetPoint(points["b"]) - obj.GetPoint(points["a"])) + centre
if is_quad:
up = (obj.GetPoint(points["b"]) - obj.GetPoint(points["c"])) + centre
else:
up = (obj.GetPoint(points["c"]) - obj.GetPoint(points["b"])) + centre
elif axis_mode == "diagonal_a":
align = obj.GetPoint(points["a"])
if is_quad:
up = (obj.GetPoint(points["b"]) - obj.GetPoint(points["d"])) + centre
else:
up = (obj.GetPoint(points["c"]) - obj.GetPoint(points["b"])) + centre
elif axis_mode == "diagonal_b":
align = obj.GetPoint(points["b"])
if is_quad:
up = (obj.GetPoint(points["c"]) - obj.GetPoint(points["a"])) + centre
else:
up = (obj.GetPoint(points["c"]) - obj.GetPoint(points["a"])) + centre
# Create polygon object
poly_obj = c4d.BaseObject(c4d.Opolygon)
poly_obj.SetName(f"Poly_{poly_id:03d}")
doc.InsertObject(poly_obj, container, None)
# Set matrix
aim_axis = "x" if axis_mode == "smooth" else "y"
up_axis = "y" if axis_mode == "smooth" else "x"
ml = AimAxis(centre, align, aim_axis, up, up_axis)
if ml is None:
ml = c4d.Matrix()
ml.off = centre
poly_obj.SetMl(ml)
Rel2Frozen(poly_obj)
# Set points
poly_obj.ResizeObject(pcnt, 1)
new_points = {}
for key in points:
global_pos = Pos2Off(obj, obj.GetPoint(points[key]))
new_points[key] = Off2Pos(poly_obj, global_pos)
poly_obj.SetPoint(0, new_points["a"])
poly_obj.SetPoint(1, new_points["b"])
poly_obj.SetPoint(2, new_points["c"])
if is_quad:
poly_obj.SetPoint(3, new_points["d"])
new_cpoly = c4d.CPolygon(0, 1, 2, 3) if is_quad else c4d.CPolygon(0, 1, 2)
poly_obj.SetPolygon(0, new_cpoly)
# Copy UVW data from source polygon to this split piece
if has_uvw:
CopyAllUVWTags(obj, poly_obj, poly_id, 0)
# Copy material tags
CopyMaterialTags(obj, poly_obj)
poly_objects.append(poly_obj)
# Hide original object
obj[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 1
return container, poly_objects
# ============================================================================
# HIERARCHY ARRANGEMENT
# ============================================================================
def ArrangeBySelection(poly_objects, container):
"""Arrange polygon objects in selection order (hierarchical chain)"""
if len(poly_objects) < 2:
return False
# Clone objects
clones = [obj.GetClone() for obj in poly_objects]
# Insert first clone
doc.InsertObject(clones[0], container, None)
# Insert remaining as children of previous
for i in range(1, len(clones)):
doc.InsertObject(clones[i], clones[i-1], None)
clones[i].SetMl(FromMgToMl(poly_objects[i-1], poly_objects[i], poly_objects[i]))
Abs2Frozen(clones[i])
# Remove originals
for obj in poly_objects:
obj.Remove()
return True
def ArrangeByNeighbor(poly_objects, container, tolerance=5.0):
"""Arrange polygon objects by detecting neighbors through shared points"""
if len(poly_objects) < 2:
return False
sel = list(poly_objects)
first = sel[0]
ordered = [first]
# Build chain by finding neighbors
while len(sel) > 1:
sel.remove(first)
next_obj = None
mpnts = GetGlobalPointsTuple(first)
# Find object with 2 matching points
for obj in sel:
count = 0
cpnts = GetGlobalPointsTuple(obj)
for cpoint in cpnts:
for mpoint in mpnts:
if GetDistance(cpoint, mpoint) < tolerance:
count += 1
if count == 2:
next_obj = obj
break
if next_obj is not None:
break
if next_obj is None:
gui.MessageDialog(
"Cannot find connected neighbors!\n\n"
"Possible issues:\n"
"- Polygons not connected in a chain\n"
"- First polygon not at start/end\n"
"- Tolerance too small\n"
"- Polygons don't share exactly 2 points"
)
return False
else:
ordered.append(next_obj)
first = next_obj
# Clone in correct order
clones = [obj.GetClone() for obj in ordered]
# Insert first clone
doc.InsertObject(clones[0], container, None)
# Insert remaining as children
for i in range(1, len(clones)):
Frozen2Rel(clones[i])
doc.InsertObject(clones[i], clones[i-1], None)
clones[i].SetMl(FromMgToMl(ordered[i-1], ordered[i], ordered[i]))
Rel2Frozen(clones[i])
# Remove originals
for obj in ordered:
obj.Remove()
return True
# ============================================================================
# ALIGNMENT (HINGE POSITIONING)
# ============================================================================
def AlignPolygonChain(root, tolerance=5.0, up_str="z"):
"""Align polygon chain so rotation axes are on shared edges"""
child = root
obj_list = []
mat_list = []
pnts_list = []
while child is not None:
# Get points from current and neighbor
if child == root:
mpnts = GetGlobalPointsTuple(child)
neighbor = child.GetDown() if child.GetDown() else child.GetUp()
cpnts = GetGlobalPointsTuple(neighbor) if neighbor else mpnts
else:
mpnts = GetGlobalPointsTuple(child)
neighbor = child.GetUp() if child.GetUp() else child.GetDown()
cpnts = GetGlobalPointsTuple(neighbor) if neighbor else mpnts
# Find mutual and own points
mutpnts = []
for cpoint in cpnts:
for mpoint in mpnts:
if GetDistance(cpoint, mpoint) < tolerance:
mutpnts.append(mpoint)
break
ownpnts = [p for p in mpnts if p not in mutpnts]
# Must have exactly 2 shared points
if len(mutpnts) != 2:
return False
# Calculate alignment
pcnt = child.GetPointCount()
if pcnt == 4:
if child == root:
off = (ownpnts[0] + ownpnts[1]) / 2
aim = (mutpnts[0] + mutpnts[1]) / 2
else:
off = (mutpnts[0] + mutpnts[1]) / 2
aim = (ownpnts[0] + ownpnts[1]) / 2
elif pcnt == 3:
if child == root:
off = ownpnts[0]
aim = (mutpnts[0] + mutpnts[1]) / 2
else:
off = (mutpnts[0] + mutpnts[1]) / 2
aim = ownpnts[0]
else:
return False
# Calculate up vector
temp_mat = c4d.Matrix()
temp_mat.off = off
temp_mat.v1 = child.GetMg().v1
temp_mat.v2 = child.GetMg().v2
temp_mat.v3 = child.GetMg().v3
up_local = c4d.Vector(0, 0, 50)
up = Pos2Off(temp_mat, up_local)
# Create alignment matrix
mat = AimAxis(off, aim, "x", up, up_str)
if mat is None:
return False
# Transform points
pnts = tuple(Off2Pos(mat, vec) for vec in mpnts)
obj_list.append(child)
mat_list.append(mat)
pnts_list.append(pnts)
child = child.GetDown()
# Apply transformations
for i in range(len(obj_list)):
obj_list[i].SetMg(mat_list[i])
for ii in range(len(pnts_list[i])):
obj_list[i].SetPoint(ii, pnts_list[i][ii])
obj_list[i].Message(c4d.MSG_UPDATE)
Abs2Frozen(obj_list[i])
return True
# ============================================================================
# UNFOLD CONTROLLER TAG CREATION
# ============================================================================
def CreateUnfoldControllerTag(container):
"""Create and attach the unfold controller Python tag to the container"""
# Create Python tag
tag = c4d.BaseTag(c4d.Tpython)
tag.SetName("Unfold_Controller")
# Set the Python code
tag[c4d.TPYTHON_CODE] = UNFOLD_TAG_CODE
# Create user data for the TAG (not the container)
# ID 1: Use Effect (Bool)
bc_use = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BOOL)
bc_use[c4d.DESC_NAME] = "Use Effect"
bc_use[c4d.DESC_DEFAULT] = True
tag.AddUserData(bc_use)
# ID 2: Unfold Amount (Float, 0-1)
bc_amount = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
bc_amount[c4d.DESC_NAME] = "Unfold Amount"
bc_amount[c4d.DESC_MIN] = 0.0
bc_amount[c4d.DESC_MAX] = 1.0
bc_amount[c4d.DESC_MINSLIDER] = 0.0
bc_amount[c4d.DESC_MAXSLIDER] = 1.0
bc_amount[c4d.DESC_DEFAULT] = 0.0
bc_amount[c4d.DESC_STEP] = 0.01
bc_amount[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
tag.AddUserData(bc_amount)
# ID 3: Unfold Fade (Float, 0-1)
bc_fade = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
bc_fade[c4d.DESC_NAME] = "Unfold Fade"
bc_fade[c4d.DESC_MIN] = 0.0
bc_fade[c4d.DESC_MAX] = 1.0
bc_fade[c4d.DESC_MINSLIDER] = 0.0
bc_fade[c4d.DESC_MAXSLIDER] = 1.0
bc_fade[c4d.DESC_DEFAULT] = 0.5
bc_fade[c4d.DESC_STEP] = 0.01
bc_fade[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
tag.AddUserData(bc_fade)
# ID 4: Rotation Axis (Long - Cycle)
bc_axis = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
bc_axis[c4d.DESC_NAME] = "Rotation Axis"
bc_axis[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
bc_axis[c4d.DESC_DEFAULT] = 0
cycle = c4d.BaseContainer()
cycle.SetString(0, "Y")
cycle.SetString(1, "X")
cycle.SetString(2, "Z")
bc_axis[c4d.DESC_CYCLE] = cycle
tag.AddUserData(bc_axis)
# ID 5: Clockwise (Bool)
bc_clockwise = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BOOL)
bc_clockwise[c4d.DESC_NAME] = "Clockwise"
bc_clockwise[c4d.DESC_DEFAULT] = True
tag.AddUserData(bc_clockwise)
# ID 6: Invert Order (Bool)
bc_invert = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BOOL)
bc_invert[c4d.DESC_NAME] = "Invert Order"
bc_invert[c4d.DESC_DEFAULT] = False
tag.AddUserData(bc_invert)
# ID 7: Object Count (Long - Read Only)
bc_count = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
bc_count[c4d.DESC_NAME] = "Object Count"
bc_count[c4d.DESC_DEFAULT] = 0
bc_count[c4d.DESC_EDITABLE] = False
tag.AddUserData(bc_count)
# Attach tag to container
container.InsertTag(tag)
return tag
# ============================================================================
# MAIN DIALOG
# ============================================================================
class UnfoldDialog(gui.GeDialog):
"""Main dialog for polygon unfold setup"""
# IDs
GRP_MAIN = 1000
TXT_TITLE = 1001
GRP_SPLIT = 1100
TXT_SPLIT = 1101
CMB_AXIS = 1102
BTN_SPLIT = 1103
GRP_ARRANGE = 1200
TXT_ARRANGE = 1201
BTN_BY_SEL = 1202
BTN_BY_NEIGHBOR = 1203
TXT_TOL = 1204
EDT_TOL = 1205
GRP_ALIGN = 1300
TXT_ALIGN = 1301
BTN_ALIGN = 1302
CMB_UP = 1303
GRP_BUTTONS = 1400
BTN_FINISH = 1401
BTN_CLOSE = 1402
# Combo IDs
AXIS_SMOOTH = 2001
AXIS_STRAIGHT = 2002
AXIS_DIAG_A = 2003
AXIS_DIAG_B = 2004
UP_Z = 3001
UP_MZ = 3002
def __init__(self):
super(UnfoldDialog, self).__init__()
self.axis_mode = "smooth"
self.tolerance = 5.0
self.up_vector = "z"
self.container = None
self.poly_objects = None
self.setup_complete = False
def CreateLayout(self):
self.SetTitle("Polygon Unfold Setup (UVW Preserved)")
self.GroupBegin(self.GRP_MAIN, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1, rows=0)
self.GroupBorderSpace(10, 10, 10, 10)
# Title
self.AddStaticText(self.TXT_TITLE, c4d.BFH_CENTER,
name="Polygon Unfold Setup Wizard")
self.AddSeparatorH(0, c4d.BFH_SCALEFIT)
# STEP 1: Split
self.GroupBegin(self.GRP_SPLIT, c4d.BFH_SCALEFIT, cols=2, rows=0)
self.GroupBorder(c4d.BORDER_GROUP_IN)
self.GroupBorderSpace(5, 5, 5, 5)
self.AddStaticText(self.TXT_SPLIT, c4d.BFH_LEFT,
name="STEP 1: Split Selected Polygons")
self.AddStaticText(0, c4d.BFH_LEFT, name="")
self.AddStaticText(0, c4d.BFH_LEFT, name="Axis Alignment:")
self.AddComboBox(self.CMB_AXIS, c4d.BFH_SCALEFIT)
self.AddChild(self.CMB_AXIS, self.AXIS_SMOOTH, "Smooth (Default)")
self.AddChild(self.CMB_AXIS, self.AXIS_STRAIGHT, "Straight Edge")
self.AddChild(self.CMB_AXIS, self.AXIS_DIAG_A, "Diagonal A")
self.AddChild(self.CMB_AXIS, self.AXIS_DIAG_B, "Diagonal B")
self.AddButton(self.BTN_SPLIT, c4d.BFH_SCALEFIT, name="Split Polygons")
self.AddStaticText(0, c4d.BFH_LEFT, name="(Preserves UVWs)")
self.GroupEnd()
# STEP 2: Arrange
self.GroupBegin(self.GRP_ARRANGE, c4d.BFH_SCALEFIT, cols=2, rows=0)
self.GroupBorder(c4d.BORDER_GROUP_IN)
self.GroupBorderSpace(5, 5, 5, 5)
self.AddStaticText(self.TXT_ARRANGE, c4d.BFH_LEFT,
name="STEP 2: Arrange Into Chain")
self.AddStaticText(0, c4d.BFH_LEFT, name="")
self.AddButton(self.BTN_BY_SEL, c4d.BFH_SCALEFIT,
name="Use Selection Order")
self.AddStaticText(0, c4d.BFH_LEFT,
name="(Select polygons in order)")
self.AddButton(self.BTN_BY_NEIGHBOR, c4d.BFH_SCALEFIT,
name="Auto-Detect Neighbors")
self.AddStaticText(0, c4d.BFH_LEFT,
name="(Finds connected chain)")
self.AddStaticText(self.TXT_TOL, c4d.BFH_LEFT, name="Point Tolerance:")
self.AddEditNumber(self.EDT_TOL, c4d.BFH_SCALEFIT)
self.GroupEnd()
# STEP 3: Align
self.GroupBegin(self.GRP_ALIGN, c4d.BFH_SCALEFIT, cols=2, rows=0)
self.GroupBorder(c4d.BORDER_GROUP_IN)
self.GroupBorderSpace(5, 5, 5, 5)
self.AddStaticText(self.TXT_ALIGN, c4d.BFH_LEFT,
name="STEP 3: Align Hinges (Optional)")
self.AddStaticText(0, c4d.BFH_LEFT, name="")
self.AddStaticText(0, c4d.BFH_LEFT, name="Up Vector:")
self.AddComboBox(self.CMB_UP, c4d.BFH_SCALEFIT)
self.AddChild(self.CMB_UP, self.UP_Z, "Z")
self.AddChild(self.CMB_UP, self.UP_MZ, "-Z")
self.AddButton(self.BTN_ALIGN, c4d.BFH_SCALEFIT,
name="Align Chain Hinges")
self.AddStaticText(0, c4d.BFH_LEFT,
name="(Positions rotation axes)")
self.GroupEnd()
# Bottom buttons
self.AddSeparatorH(0, c4d.BFH_SCALEFIT)
self.GroupBegin(self.GRP_BUTTONS, c4d.BFH_CENTER, cols=2)
self.AddButton(self.BTN_FINISH, c4d.BFH_SCALEFIT, name="Finish & Add Controller")
self.AddButton(self.BTN_CLOSE, c4d.BFH_SCALEFIT, name="Close")
self.GroupEnd()
self.GroupEnd()
return True
def InitValues(self):
self.SetLong(self.CMB_AXIS, self.AXIS_SMOOTH)
self.SetReal(self.EDT_TOL, self.tolerance)
self.SetLong(self.CMB_UP, self.UP_Z)
return True
def Command(self, id, msg):
# Update values
if id == self.CMB_AXIS:
val = self.GetLong(self.CMB_AXIS)
if val == self.AXIS_SMOOTH:
self.axis_mode = "smooth"
elif val == self.AXIS_STRAIGHT:
self.axis_mode = "straight"
elif val == self.AXIS_DIAG_A:
self.axis_mode = "diagonal_a"
elif val == self.AXIS_DIAG_B:
self.axis_mode = "diagonal_b"
if id == self.EDT_TOL:
self.tolerance = self.GetReal(self.EDT_TOL)
if id == self.CMB_UP:
val = self.GetLong(self.CMB_UP)
self.up_vector = "z" if val == self.UP_Z else "-z"
# STEP 1: Split
if id == self.BTN_SPLIT:
obj = doc.GetActiveObject()
if obj is None or obj.GetType() != c4d.Opolygon:
gui.MessageDialog("Please select a polygon object!")
return True
sel = GetSelectedPolygons(obj)
if sel is None:
gui.MessageDialog("Please select some polygons first!")
return True
c4d.StopAllThreads()
doc.StartUndo()
doc.AddUndo(c4d.UNDOTYPE_NEW, obj)
result = SplitPolygons(obj, self.axis_mode)
if result:
self.container, self.poly_objects = result
# Check if UVWs were preserved
uvw_status = "UVW tags copied!" if GetUVWTag(obj) else "No UVW tags found."
gui.MessageDialog(
f"Success!\n\n"
f"Split {len(self.poly_objects)} polygons.\n"
f"Container: {self.container.GetName()}\n"
f"{uvw_status}\n\n"
f"Next: Choose arrangement method."
)
doc.EndUndo()
c4d.EventAdd()
# STEP 2: Arrange
if id == self.BTN_BY_SEL:
if self.poly_objects is None:
gui.MessageDialog("Please split polygons first (Step 1)!")
return True
c4d.StopAllThreads()
doc.StartUndo()
if ArrangeBySelection(self.poly_objects, self.container):
# Get new chain
self.poly_objects = []
child = self.container.GetDown()
if child and child.GetDown():
obj = child.GetDown()
while obj:
self.poly_objects.append(obj)
obj = obj.GetDown()
self.setup_complete = True
gui.MessageDialog(
"Success!\n\n"
"Arranged polygons in selection order.\n"
"Polygons are now in a hierarchical chain.\n"
"UVW coordinates preserved.\n\n"
"Next: Optionally align hinges (Step 3)\n"
"or click 'Finish & Add Controller'."
)
doc.EndUndo()
c4d.EventAdd()
if id == self.BTN_BY_NEIGHBOR:
if self.poly_objects is None:
gui.MessageDialog("Please split polygons first (Step 1)!")
return True
c4d.StopAllThreads()
doc.StartUndo()
if ArrangeByNeighbor(self.poly_objects, self.container, self.tolerance):
# Get new chain
self.poly_objects = []
child = self.container.GetDown()
if child and child.GetDown():
obj = child.GetDown()
while obj:
self.poly_objects.append(obj)
obj = obj.GetDown()
self.setup_complete = True
gui.MessageDialog(
"Success!\n\n"
"Auto-detected and arranged neighbors.\n"
"Polygons are now in a hierarchical chain.\n"
"UVW coordinates preserved.\n\n"
"Next: Optionally align hinges (Step 3)\n"
"or click 'Finish & Add Controller'."
)
doc.EndUndo()
c4d.EventAdd()
# STEP 3: Align
if id == self.BTN_ALIGN:
if self.poly_objects is None or len(self.poly_objects) == 0:
gui.MessageDialog("Please arrange polygons first (Step 2)!")
return True
c4d.StopAllThreads()
doc.StartUndo()
# Get root of chain
root = self.poly_objects[0]
if AlignPolygonChain(root, self.tolerance, self.up_vector):
gui.MessageDialog(
"Success!\n\n"
"Aligned rotation axes on shared edges.\n"
"Chain is ready for unfolding.\n"
"UVW coordinates maintained.\n\n"
"Click 'Finish & Add Controller' to complete setup."
)
else:
gui.MessageDialog(
"Alignment failed!\n\n"
"Possible issues:\n"
"- Objects don't share exactly 2 points\n"
"- Tolerance too small\n"
"- Objects not 3 or 4 points each\n\n"
"Try adjusting tolerance and re-arrange."
)
doc.EndUndo()
c4d.EventAdd()
# FINISH BUTTON
if id == self.BTN_FINISH:
if not self.setup_complete or self.container is None:
gui.MessageDialog(
"Setup incomplete!\n\n"
"Please complete steps 1 and 2:\n"
"1. Split polygons\n"
"2. Arrange into chain\n\n"
"Step 3 (alignment) is optional."
)
return True
c4d.StopAllThreads()
doc.StartUndo()
doc.AddUndo(c4d.UNDOTYPE_CHANGE, self.container)
# Create and attach the unfold controller tag
tag = CreateUnfoldControllerTag(self.container)
doc.EndUndo()
c4d.EventAdd()
# Select the container for easy access
doc.SetActiveObject(self.container)
gui.MessageDialog(
"Setup Complete!\n\n"
f"Created 'Unfold_Controller' tag on:\n"
f"{self.container.GetName()}\n\n"
"Controls (on the tag):\n"
"- Use Effect: Enable/disable animation\n"
"- Unfold Amount: 0-1 animation progress\n"
"- Unfold Fade: Overlap amount\n"
"- Rotation Axis: Which axis to rotate\n"
"- Clockwise: Rotation direction\n"
"- Invert Order: Reverse sequence\n\n"
"UVW coordinates preserved!\n"
"Textures will appear continuous.\n\n"
"Animate 'Unfold Amount' to create the effect!"
)
self.Close()
if id == self.BTN_CLOSE:
self.Close()
return True
# ============================================================================
# MAIN FUNCTION
# ============================================================================
def main():
"""Launch the unfold setup dialog"""
obj = doc.GetActiveObject()
if obj is None:
gui.MessageDialog(
"No object selected!\n\n"
"Please select a polygon object with\n"
"polygons selected that you want to unfold."
)
return False
if obj.GetType() != c4d.Opolygon:
gui.MessageDialog(
f"'{obj.GetName()}' is not a polygon object!\n\n"
"Please select a polygon object."
)
return False
sel = GetSelectedPolygons(obj)
if sel is None:
gui.MessageDialog(
"No polygons selected!\n\n"
"Please select the polygons you want\n"
"to split and unfold.\n\n"
"Tip: Select polygons in the order\n"
"you want them to unfold for best results."
)
return False
# Open dialog
dlg = UnfoldDialog()
dlg.Open(c4d.DLG_TYPE_MODAL, defaultw=400, defaulth=500)
c4d.EventAdd()
return True
if __name__ == '__main__':
main()