How to evenly distribute objects of different 'width'
-
Hi guys,
so, right now I'm at a point where think I give in. After spending hours after hours fiddeling around with matrcies, global and local positions I came to the conclusion I need someones help.
What I'm trying to achieve.
I'm currently prototyping a script that evenly distributes objects with potentially different widths. Quite similiar to the align tools known from Photoshop, Illustrator, etc.So far I have a solution that works only for objects aligned to the world frame, i.e. as soon as the objects are rotated this works no longer.
I suspect that I somehow have to translate my computations in relation to the global transform of the objects in question. Right now I'm running out of ideas how to do so.
I really hope someone can provide some insight here.
I have attached the scene file as well as a screenshot illustrating the situation. Please find the code I'm using below.
Cheers,
Sebastianfrom typing import Optional import c4d import itertools doc: c4d.documents.BaseDocument # The active document op: Optional[c4d.BaseObject] # The active object, None if unselected def main() -> None: objs = doc.GetActiveObjects(0) if not objs: return # Store the x component of selected objs' positions positions = [ obj.GetMg().off.x for obj in reversed(objs) ] # Store the x component of selected objs' radius radii = [ obj.GetRad().x for obj in reversed(objs) ] # Store the x component of selected objs' diameter diameters = [ radius * 2 for radius in radii ] # Compute positions so that they sit next to each other without a gap stacked = [ diameter - radius for diameter, radius in zip(itertools.accumulate(diameters), radii) ] count = len(radii) width = sum(diameters) span = min(positions) - radii[0], max(positions) + radii[-1] start, end = span difference = end - start - width spacing = difference / (count-1) # Compute the final position for selected objs positions = [ start + position + (spacing * i) for i, position in enumerate(stacked) ] print("." * 79) print(radii) print(diameters) print(stacked) print(width) print(span) print(spacing) print(positions) print("." * 79) doc.StartUndo() # Set the final position computed before for obj, posx in zip(reversed(objs), positions): doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj) mg = obj.GetMg() mg.off = c4d.Vector(posx, mg.off.y, mg.off.z) obj.SetMg(mg) doc.EndUndo() c4d.EventAdd() if __name__ == "__main__": main()
-
Hi Sebastian,
Your question lies in the algorithmic field and hence according to our guidelines is generally out of scope of our support. I would suggest you checking our Matrix Manual to get better understanding of how transforms work in Cinema4D.
Regarding the subject, it's likely that you don't properly handle object positions. You collect only X-components of object positions, whilst your expected result (as far as I correctly understood your goal) requires addressing position projections on the line between first and last objects.
Please find below a draft (and not optimized) code snippet that presumably does what you're trying to achieve. The main difference from your code is in new positions calculation. Namely, I'm taking the direction vector and then position objects along it while keeping correct distances (or better say projections) from the first object.
Note, the code doesn't reflect to object size changes that come from the Transform: Scale property (as well as Freeze Transform: Scale). You will need to address them yourself. Additionally, calculation of actual objects' bounding box can sometimes be quite tricky. Please, familiarize yourself with one of related threads: How to get the bounding box for the whole scene
Cheers,
Iliafrom typing import Optional import c4d doc: c4d.documents.BaseDocument # The active document op: Optional[c4d.BaseObject] # The active object, None if unselected def main() -> None: obj: c4d.BaseObject objs = doc.GetActiveObjects(0) if not objs or len(objs) < 3: return # Sort objects along required axis objs.sort(key=lambda o: o.GetMg().off.x) objMgs: list[c4d.Matrix] = [obj.GetMg() for obj in objs] # .GetRad() gives you radii in local space (i.e. transform matrix invariant) radii: list[float] = [o.GetRad().x for o in objs] # Calculate unit direction vector for ray starting at the position of objs[0] dir: c4d.Vector = (objMgs[-1].off - objMgs[0].off).GetNormalized() # Project object positions onto direction vector alignAxisPositions = [c4d.Vector.Dot(mg.off, dir) for mg in objMgs] # Get scalar-positions of left and right points left = alignAxisPositions[0] - radii[0] right = alignAxisPositions[-1] + radii[-1] objectsCount = len(objs) objectsWidth = sum(radii) * 2 # Calculate spacing in between of objects spacing = ((right - left) - objectsWidth) / (objectsCount - 1) # Calculate new c4d.Vector positions for the objects objNewPositions: list[c4d.Vector] = [objMgs[0].off] for idx in range(1, objectsCount): distanceFromPreviousObject: float = radii[idx - 1] + radii[idx] + spacing previousPosition: c4d.Vector = objNewPositions[idx - 1] objNewPositions.append(previousPosition + dir * distanceFromPreviousObject) # Actually move objects doc.StartUndo() for obj, newPos in zip(objs, objNewPositions): doc.AddUndo(c4d.UNDOTYPE_CHANGE, obj) objMg = obj.GetMg() objMg.off = newPos obj.SetMg(objMg) doc.EndUndo() c4d.EventAdd() return if __name__ == "__main__": main()
-
Hi @i_mazlov,
thank you for the explanation as well as the code sample. That really pointed me in the right direction. Especially the projection part along with the topic about bounding box calculations.
Since
GetRad()
is oriented with the object I can not really use that for a "bounding box". I then thought I could use the custom class @ferdinand provided here. Well, turns out that this fails as soon as the axis of an object is no longer in the center of the points.So I ended up starting at @ferdinand example and building my own bounding box via iterating object points. This finally gave me a bounding box that "always" sits in the "center" of an object - no matter where the axis lives or it that object has "rotated points".
So far so good. However, I noticed that the code you provided - though meant as a starting point and no optimized implementation - produces different results for the same objects when these objects are rearranged.
To have a better understanding what I'm talking about, could you be so kind and have a look at the attached scene file?
You will find that the spacing between objects is not always evenly distributed. I wonder why that is as there is e.g. no scaling involved. Just simple cubes with a different order of positions.
Thank you for you support.
Sebastian -
Hi Sebastian,
Great that you made progress with the bounding box calculation!
Regarding the distribution part. Yes, the inconsistency comes from taking into account only X-component of GetRad() while operating on objects in all 3 dimensions (and then checking the results again in the X-projection view).
You need to comprehend the exact result that you'd like to have, or in other words ask yourself a question, what does it mean for the objects to be "evenly distributed"?
- Along which axis you'd like to distribute them?
- What's the metric for the "width" of the object, is it enough to grab xyz-bounding box of the object (pic 1, left) or you need to calculate bounding box that's aligned with the axis of objects distribution (pic 1, right)? As you can see on the picture the bbox size can vary drastically.
- What does "evenly" mean: same spacing between consequent "right" and "left" positions or same distance between object positions, or same distance between centers of object's bounding box? If first or third, check question #2.
Picture 1. Purple - axis of distribution, Half-transparent - bounding box. Left - bounding box in world coordinates, Right - bounding box aligned with the distribution axisThe path you would follow would stay the same as in the example:
- Define the axis of distribution.
- (Optionally) Rotate objects to be aligned with the axis of distribution
- Project objects onto this ray.
- Calculate correct spacing (refer to previous set of questions)
- Redistribute aligned objects with the correct spacing along the axis
Regarding the script, you can try calculating radii based on maximum of bbox size rather than just X-component, but it would still not be an optimal solution since there're so many unknowns in the problem statement. Just change the line:
radii: list[float] = [o.GetRad().x for o in objs]
to the following
radiiVector: list[c4d.Vector] = [o.GetRad() for o in objs] radii: list[float] = [max(r.x, r.y, r.z) for r in radiiVector]
Cheers,
Ilia -
I was able to create the same outcome with this script in a python generator and sever cube objects as children of the generator. Everything works great and I am able to change width and offset the axis and all my child objects stack nicely side by side. This is very similar to the popular Autolayout effect in Figma.
Unfortunately I cannot get the correct center from utils.BBox so if one of my child objects is a hierarchy I can't get the correct outcomes.
This code might be a bit easier to read for python juniors like myself.
def main(): parent = op child = parent.GetChildren() count = len(child) #number of assets layout = c4d.Vector( 0 ,0 , 0 ) padding = op[c4d.ID_USERDATA,3] for i in range(0,count): #loop through all children box = c4d.utils.GetBBox(child[i], child[i].GetMg()) #BBox is a tuple with (center,radius) pre = c4d.utils.GetBBox(child[i-1], child[i-1].GetMg()) #BBox is a tuple with (center,radius) center = child[i].GetMp() preCenter = child[i-1].GetMp() layout += box[1] - center + pre[1] + preCenter + padding child[i][c4d.ID_BASEOBJECT_ABS_POSITION,c4d.VECTOR_X] = layout.x continue
-
Hi @gsmetzer,
please have a look at the related thread (How to get the bounding box for the whole scene) where your issue was explained in details by @ferdinand
@ferdinand said in How to get the bounding box for the whole scene:
a BaseObject will also not include its descendants in its bounding box computation
So, one must carry out such computations oneself
Cheers,
Ilia