Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    How to evenly distribute objects of different 'width'

    Cinema 4D SDK
    python s26 windows
    3
    6
    1.9k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • H
      HerrMay
      last edited by

      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,
      Sebastian

      distribute.c4d

      distribute.jpg

      from 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()
      
      1 Reply Last reply Reply Quote 0
      • i_mazlovI
        i_mazlov
        last edited by i_mazlov

        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,
        Ilia

        from 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()
        

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 1
        • H
          HerrMay
          last edited by

          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

          distribute.c4d

          1 Reply Last reply Reply Quote 0
          • i_mazlovI
            i_mazlov
            last edited by i_mazlov

            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"?

            1. Along which axis you'd like to distribute them?
            2. 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.
            3. 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.

            586f5702-4b95-415c-8516-dd75e25efa46-image.png
            Picture 1. Purple - axis of distribution, Half-transparent - bounding box. Left - bounding box in world coordinates, Right - bounding box aligned with the distribution axis

            The path you would follow would stay the same as in the example:

            1. Define the axis of distribution.
            2. (Optionally) Rotate objects to be aligned with the axis of distribution
            3. Project objects onto this ray.
            4. Calculate correct spacing (refer to previous set of questions)
            5. 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

            MAXON SDK Specialist
            developers.maxon.net

            1 Reply Last reply Reply Quote 0
            • G
              gsmetzer
              last edited by gsmetzer

              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
                
              
              
              i_mazlovI 1 Reply Last reply Reply Quote 0
              • i_mazlovI
                i_mazlov @gsmetzer
                last edited by i_mazlov

                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

                MAXON SDK Specialist
                developers.maxon.net

                1 Reply Last reply Reply Quote 0
                • First post
                  Last post