SOLVEDHelp with Matrix Manipulation after c4d.MCOMMAND_JOIN

Hi @mogh,

unfortunately it is not quite clear to me what you are trying to do, because both your snippets are not executable, i.e. are just snippets or include pseudo-code. However, from this general setting and your wording in your first posting, I assume you are simply trying "to move the axis of an object without moving its points", i.e. do what the move axis tool does. Below you find a simple script which shows you how to change the transform of a `PointObject` without changing the global coordinates of its points.

If this is not what you are looking for, I would ask you to show us code that is executable and to explain in more detail what kind of transform you are looking for.

Cheers,
Ferdinand

``````"""Demonstrates how to "move" the transform of a point object.

"Moves" the transform of a PointObject without changing the global
coordinates of its points, i.e. does what the move axis tool does.

You have to select a point object and then another object and it will move
the axis of the point object to the scond one.
"""

import c4d

def set_point_object_transform(node, transform):
"""Sets the global transform of a point object while keeping its points
in place.

Args:
node (c4d.PointObject): The point object to move the axis for.
transform (c4d.Matrix): The new global transform for the object.

Raises:
TypeError: When node or transform are not of specified type.
"""
if (not isinstance(node, c4d.PointObject) or
not isinstance(transform, c4d.Matrix)):
msg = f"Illegal argument types: {type(node)}{type(transform)}"
raise TypeError(msg)

mg = node.GetMg()
# Move the points in the global frame and then into the new frame.
points = [p * mg * ~transform for p in node.GetAllPoints()]
# Set the points and stuff ;)
node.SetAllPoints(points)
node.Message(c4d.MSG_UPDATE)
node.SetMg(transform)

def main():
"""Runs set_point_object_transform() on the current selection.
"""
nodes = doc.GetActiveObjects(0)
if len(nodes) < 2:
return

node, transform = nodes[0], nodes[1].GetMg()
set_point_object_transform(node, transform)

if __name__ == "__main__":
main()
``````

Thank you for your patience and time Zipit.

I implemented your code into mine, but sadly it does the same as mine ... all the polygon objects move and rotate.

I will make a stripped down version of the code so you can have a better look at it.

Thank you for your time.
kind regards
mogh

So this is the compact version I could come up with. (including your Code @Zipit)

Please add some poly Objects under Nulls called "Axis" into your scene, an nest them arbitrary, with random rotation and location to se the problem when they are joined.

Thank you for your time.

``````#!py3
import c4d, sys, os
from c4d import gui
from c4d.documents import GetActiveDocument
#Version 1.4 Striped version
# This Script needs Null-Objects Called "Axis" with polygon objects to run

def GetNextObject(op):
if not op: return
if op.GetDown(): return op.GetDown()
while op.GetUp() and not op.GetNext():
op = op.GetUp()
return op.GetNext()

def get_all_objects (op):
allachsen_list = list()
all_objects_list = list()
while op:
if op.GetName() == 'Achsen-Objekt' or op.GetName() == 'Axis' :
allachsen_list.append(op)
all_objects_list.append(op)
op = GetNextObject(op)
return all_objects_list, allachsen_list

def JoinCommand(doc, op):
res = c4d.utils.SendModelingCommand(command = c4d.MCOMMAND_JOIN,
list = [op],
mode = c4d.MODELINGCOMMANDMODE_ALL,
doc = doc)

# Cheks if the command didn't failed
if res is False:
raise TypeError("return value of Join command is not valid")
elif res is True:
print ("Command successful. But no object.")
elif isinstance(res, list):
if c4d.GetC4DVersion() < 21000: res[0].SetAbsPos(c4d.Vector())
op.Remove()
return res[0] # Returns the first item containing the object of the list.  ??? GetClone() ???

def set_point_object_transform(node, transform):
if (not isinstance(node, c4d.PointObject) or
not isinstance(transform, c4d.Matrix)):
msg = f"Illegal argument types: {type(node)}{type(transform)}"
raise TypeError(msg)

mg = node.GetMg()
# Move the points in the global frame and then into the new frame.
points = [p * mg * ~transform for p in node.GetAllPoints()]
# Set the points and stuff ;)
node.SetAllPoints(points)
node.Message(c4d.MSG_UPDATE)
node.SetMg(transform)

def joinmanagment(n):
# n "Axis" null will be not alive in a few steps get everything we need from it
if n.GetUp() :
parent = n.GetUp()
else:
print ("No Parent To Axis Null. Probably not save to run this sript anyway.")
c4d.StatusClear()
c4d.StatusSetText ('No Parent found! - Probalby mo CAD import Doc. Script Stopped.')
exit()
return False

parentmg = n.GetMg()
newobject = JoinCommand(doc, n) # combine the poly objects

if not newobject.IsAlive():
raise TypeError("Object is not alive.")
return False

newobject.SetName(str(parent.GetName()))
newobject.InsertUnder(parent)

#node, transform = nodes[0], nodes[1].GetMg()
set_point_object_transform(newobject, parentmg)

def main():
c4d.CallCommand(13957) # Konsole löschen
doc = GetActiveDocument()
op = doc.GetFirstObject()

c4d.StatusSetSpin()
all_objects, allachsen = get_all_objects(op) # get two lists
null_counter = len(allachsen)

if null_counter == 0: # check if found something to do.
c4d.StatusClear()
c4d.StatusSetText ('No Axis Objects found, nothing to do here.')
print ("No Axis Objects found, nothing to do here.")
exit()

counter = len(all_objects)
secondcounter = 0
c4d.StatusSetText ('%s Objects are processed.' %(null_counter))

for n in allachsen:
secondcounter += 1
c4d.StatusSetBar(100*secondcounter/counter) #statusbar
if joinmanagment(n) == False:
break

c4d.StatusClear()
c4d.EventAdd() # update cinema 4d
print ('END OF SCRIPT')
return True

if __name__=='__main__':
main()
``````

Hi:

The merge object simply generates a new polygon object, because the merge in the modeling command cannot be used, so it can be resolved in other ways.The answer to your question is, in fact, how the connection generator works.I've written it out, but I can't generate an N-gon face.Create a new blank object, the object to be connected as a subset of the blank object, and then click the middle key of the mouse to select all blank objects and all subsets to achieve the effect of connection generator.

``````import c4d

#e-mail : [email protected]

def main():
nodes = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
if nodes == None : return
Object = [i for i in nodes
if i.GetType() == 5100]

node = c4d.BaseObject(5100)
doc.InsertObject(node)

points = []

Number_One = 0
while Number_One < len(Object):

for i in Object[Number_One].GetAllPoints():

n = i * Object[Number_One].GetMg()
points.append(n * ~node.GetMg())

Number_One = Number_One + 1

Polygons = [i.GetAllPolygons() for i in Object]

Cpolygons = Polygons[0]

Polygon_Indexe = []
if len(Object) > 1:

Number_Two = 0
while Number_Two < len(Object) - 1:

Point_Count = [ Object[0].GetPointCount() ]
if Number_Two > 0:

for i in Object[:-1 * (len(Object) - Number_Two )]:
Point_Count.append(Point_Count[-1] + i.GetPointCount())

for i in Polygons[Number_Two  + 1]:

if str(i).count("d") == 1:

Polygon_Indexe.append(c4d.CPolygon(i.a + Point_Count[-1] ,i.b +Point_Count[-1] ,i.c + Point_Count[-1] ,i.d + Point_Count[-1] ))

else:

Polygon_Indexe.append(c4d.CPolygon(i.a + Point_Count[-1] ,i.b + Point_Count[-1] ,i.c +Point_Count[-1] ))

Number_Two  = Number_Two + 1

Cpolygons += Polygon_Indexe

node.ResizeObject(len(points),len(Cpolygons))
node.SetAllPoints(points)

for i in range(len(Cpolygons)):
node.SetPolygon(i,Cpolygons[i])

node.Message(c4d.MSG_UPDATE)

node.Message(c4d.MSG_UPDATE)

if __name__=='__main__':
main()
``````

@x_nerve as I understand you copy polygons and point from one object to another, which is a solution, still I do not understand why the join command would not work.

I get a polygon object from my join comand which is ok, it just needs to reposition on its old place with all the axis and nulls kept, and oriented as it was.

kind regards
mogh

Hi @mogh,

thank you @x_nerve for jumping in, we really appreciate that, but I am not quite sure that I can follow, if you think there is a bug in `MCOMMAND_JOIN`, we would ask you to report that in a separate thread, because as I said, I currently do not really fully grasp the problem.

I understand your problem better now, and the direct answer you are looking for is to change line 50 in your script from:

`points = [p * mg * ~transform for p in node.GetAllPoints()]`

to

`points = [p * ~transform for p in node.GetAllPoints()]`

You have to do that, because you are operating in local coordinates in the way the rest of the script works. At least this is my current understanding of your goals. At the end of the post you will find both the modified full script and the test scene I did run your script on.

As a more general advice, since I saw that you did delete my docstring: You should write them for all your methods/functions/classes, because this would have told you that that specific function was operating in global space. Coding is mostly an exercise in breaking stuff down into manageable parts, and doc strings help to express that "divide and conquer" strategy, rather than viewing a script as "a single thing". Which in turn then makes revisiting or debugging your code easier.

Cheers,
Ferdinand

PC12991_scene.c4d

``````#!py3
import c4d, sys, os
from c4d import gui
from c4d.documents import GetActiveDocument
#Version 1.4 Striped version
# This Script needs Null-Objects Called "Axis" with polygon objects to run

def GetNextObject(op):
if not op: return
if op.GetDown(): return op.GetDown()
while op.GetUp() and not op.GetNext():
op = op.GetUp()
return op.GetNext()

def get_all_objects (op):
allachsen_list = list()
all_objects_list = list()
while op:
if op.GetName() == 'Achsen-Objekt' or op.GetName() == 'Axis' :
allachsen_list.append(op)
all_objects_list.append(op)
op = GetNextObject(op)
return all_objects_list, allachsen_list

def JoinCommand(doc, op):
res = c4d.utils.SendModelingCommand(command = c4d.MCOMMAND_JOIN,
list = [op],
mode = c4d.MODELINGCOMMANDMODE_ALL,
doc = doc)

# Cheks if the command didn't failed
if res is False:
raise TypeError("return value of Join command is not valid")
elif res is True:
print ("Command successful. But no object.")
elif isinstance(res, list):
if c4d.GetC4DVersion() < 21000: res[0].SetAbsPos(c4d.Vector())
op.Remove()
return res[0] # Returns the first item containing the object of the list.  ??? GetClone() ???

def set_point_object_transform(node, transform):
if (not isinstance(node, c4d.PointObject) or
not isinstance(transform, c4d.Matrix)):
msg = f"Illegal argument types: {type(node)}{type(transform)}"
raise TypeError(msg)

print ("sp:", node, transform)
mg = node.GetMg()
# Move the points in the global frame and then into the new frame.
points = [p * ~transform for p in node.GetAllPoints()]
# Set the points and stuff ;)
node.SetAllPoints(points)
node.Message(c4d.MSG_UPDATE)
node.SetMg(transform)

def joinmanagment(n):
# n "Axis" null will be not alive in a few steps get everything we need from it
if n.GetUp() :
parent = n.GetUp()
else:
print ("No Parent To Axis Null. Probably not save to run this sript anyway.")
c4d.StatusClear()
c4d.StatusSetText ('No Parent found! - Probalby mo CAD import Doc. Script Stopped.')
exit()
return False

parentmg = n.GetMg()

newobject = JoinCommand(doc, n) # combine the poly objects

if not newobject.IsAlive():
raise TypeError("Object is not alive.")
return False

newobject.SetName(str(parent.GetName()))
newobject.InsertUnder(parent)
newobject.SetMg(newobject.GetMl())
# node, transform = nodes[0], nodes[1].GetMg()(add QA contact)
set_point_object_transform(newobject, parentmg)

def main():
c4d.CallCommand(13957) # Konsole löschen
doc = GetActiveDocument()
op = doc.GetFirstObject()

c4d.StatusSetSpin()
all_objects, allachsen = get_all_objects(op) # get two lists
null_counter = len(allachsen)

if null_counter == 0: # check if found something to do.
c4d.StatusClear()
c4d.StatusSetText ('No Axis Objects found, nothing to do here.')
print ("No Axis Objects found, nothing to do here.")
exit()

counter = len(all_objects)
secondcounter = 0
c4d.StatusSetText ('%s Objects are processed.' %(null_counter))

for n in allachsen:
secondcounter += 1
c4d.StatusSetBar(100*secondcounter/counter) #statusbar
if joinmanagment(n) == False:
break

c4d.StatusClear()
c4d.EventAdd() # update cinema 4d
print ('END OF SCRIPT')
return True

if __name__=='__main__':
main()
``````

@zipit thank you very much - position and rotation seem to be working now. (staying put)

another problem ocured though "NormalTag" information seems to flip and maually flipping back does not help ... so all the polygons are "shaded" weird now. --- the callcomand "connect and delete" which I want to replace because of speed keeps them intact.

Should I open a new thread or leafe the problem open ?

regarding docstring: i just wanted the code to be slim so people can read it fast without scrolling its still in my lengthy script. Will keep it next time.

Hi,

we would appreciate it if you would open a new thread for that. As a quick tip, aside from using SMC, inverting the normal of a polygon just means inverting its point index order. I have shown it for example here. But this can come with multiple problems, especially regarding texture coordinates. I think have dealt with problem here on the forum multiple times (including dealing with secondary mesh attributes like texture coordinates). If any questions remain, please feel free to open a new thread.

About the doc strings: In the end you should do what you are comfortable with. It was just a tip and a little insight on why many people find it useful.

Cheers,
Ferdinand

Hi:

@mogh

The MCOMMAND_JOIN modeling command is now ready to execute properly.The merged object cannot be directly visible in the document, so the merged object must be inserted into the document.Also, the global matrix of the merged object is the same as the first object in the parent-child list.I also got it from the forum.

``````import c4d
from c4d import utils

#e-mail : [email protected]qq.com

def main():

nodes = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
if nodes == None : return
Objects = [i for i in nodes
if i.GetType() == 5100]

settings = c4d.BaseContainer()
settings[c4d.MDATA_JOIN_MERGE_SELTAGS] = True

result = c4d.utils.SendModelingCommand(
c4d.MCOMMAND_JOIN, Objects ,
c4d.MODELINGCOMMANDMODE_ALL, settings, doc)

doc.InsertObject(result[0])

points = []

Number_One = 0
while Number_One < len(Objects):

for i in Objects[Number_One].GetAllPoints():

n = i * Objects[Number_One].GetMg()
points.append(n * ~result[0].GetMg())

Number_One = Number_One + 1

result[0].SetAllPoints(points)

result[0].Message(c4d.MSG_UPDATE)

print (result[0])

if __name__=='__main__':
main()

``````

Hi @mogh,

I initially did not want to make things super complicated here, which is why I did a bit work around the oddities of your script in my answer. Given the other threads of yours which deal with that project, it seems advisable to point out that a cleaner version of your script could look like the script attached at the end. The doc string will tell you about the pro's and con's. You can also try uncommenting the `c4d.EventAdd` in line `155` to see if you can get Cinema more into sync (regarding https://plugincafe.maxon.net/topic/13007).

Cheers,
Ferdinand

``````"""Demonstrates how to handle MCOMMAND_JOIN while keeping things in place.

You can also use the old version, but there you would have to deal with the
normal tags. This version mostly just rectifies some minor oddities in your
original code. This version does not need to transform the vertices, but
instead places the new object by the inverse transform of its former parent
(see line 123).

Things get much more complicated (and computationally expensive) when you use
the old version, since you then have to also modify the normals. The
disadvantage  of this version is that the axis placement is not "as nice".

See https://plugincafe.maxon.net/topic/13004/ for how to deal with the normal
tags.
"""

import c4d

def get_nodes_by_name(doc, patterns):
"""Returns all nodes in a document whose name appears in patterns.

Note:
Despite its name, the strings in `patterns` are not evaluated as
regular expressions at the moment. In fact this function does not
ensure that the elements in patters are strings at all.

Args:
doc (c4d.documents.BaseDocument): The document to get the nodes from.
patterns (list[str]): The name patterns to get the nodes for.

Returns:
list[c4d.BaseObjeect]: The nodes in `doc` which match patterns.

Raises:
TypeError: On invalid argument types.
"""
# Validate arguments.
if not isinstance(doc, c4d.documents.BaseDocument):
msg = f"Expected BaseDocument for `doc`. Received: {type(doc)}."
raise TypeError(msg)
if not isinstance(patterns, list):
msg = f"Expected list for `patterns`. Received: {type(patterns)}."
raise TypeError(msg)

# Get all relevant nodes.
result, visited = [], []
node = doc.GetFirstObject()
while node is not None:
# Append new nodes to the visited and result lists.
if node not in visited:
visited.append(node)
if node.GetName() in patterns and node not in result:
result.append(node)
# Traverse the scene graph depth first.
node_down = node.GetDown()
if node_down and node_down not in visited:
node = node_down
elif node.GetNext():
node = node.GetNext()
else:
node = node.GetUp()
return result

def join_with_children(node, merge_selection_tags=True):
"""Joins the children of `node` into a single object.

Also adjusts the transform of the resulting node and removes the old
geometry and inserts the new one.

Args:
node (c4d.BaseObject): The node to join the children for.
merge_selection_tags (bool, optional): If to also merge selections.
Defaults to `True`.

Returns:
bool: If the operation was successful.

Raises:
RuntimeError: When SMC failed.
TypeError: On invalid argument types.
ValueError: When `node` is not attached to a document.
"""
# Validate arguments.
if not isinstance(node, c4d.BaseObject):
msg = f"Expected BaseObject for `node`. Received: {type(node)}"
raise TypeError(msg)

doc = node.GetDocument()
if doc is None:
msg = f"Node is not attached to a document: {node}."
raise ValueError(msg)

# A special condition of yours I am just mimicking here.
parent = node.GetUp()
if parent is None:
return

# Run SMC
smc_data = c4d.BaseContainer()
smc_data[c4d.MDATA_JOIN_MERGE_SELTAGS] = merge_selection_tags
result = c4d.utils.SendModelingCommand(
command=c4d.MCOMMAND_JOIN,
list=[node],
mode=c4d.MODELINGCOMMANDMODE_ALL,
bc=smc_data,
doc=doc,
flags=c4d.MODELINGCOMMANDFLAGS_NONE)

# Evaluate SMC output.
if (not isinstance(result, list) or
len(result) == 0 or
not isinstance(result[0], c4d.BaseObject)):
msg = f"SendModelingCommand failed on: {node}"
raise RuntimeError(msg)

# This didn't not make much sense, since you did overwrite the transform
# of result[0] on a later point anyways.
# if c4d.GetC4DVersion() < 21000:
#     result[0].SetAbsPos(c4d.Vector())

# Replace the old with the new geometry.
result = result[0]
# Your renaming does not make much sense to me, since every new node
# will then just be named after its parent. i.e. "CADimport_XXX", feel
# free to uncomment, if this was intended.
# result.SetName(parent.GetName())
result.SetMg(~parent.GetMg())
result.InsertUnder(parent)
node.Remove()

def main():
"""Does the whole CAD cleanup thing.
"""
# The name patterns for the nodes to collapse.
patterns = ["Achsen-Objekt", "Axis"]
# Get all nodes that match that pattern.
targets = get_nodes_by_name(doc, patterns)
count = len(targets)

c4d.StatusSetSpin()
# Loop over these nodes.
#
# Your approach did not respect the special case when an `axis` object
# is a direct child of another `axis` object. Which will lead to
# incorrect results, since you (and I here) traverse the scene graph
# top-down-depth-first. I did not address this to keep the code relatively
# familiar.
for i, node in enumerate(targets):
# Join one of the targets and do some mild interface indication.
join_with_children(node)
# Uncomment to let Cinema update for each modification of the scene.
c4d.StatusSetText(f"{i}/{count} groups joined.")
# Clean up after ourselves.
c4d.StatusSetText("")
c4d.StatusClear()

if __name__ == '__main__':
main()
``````

I am building of your script from here on. Thank You.

FYI zipit:
While your `def get_nodes_by_name(doc, patterns)` might be saver and more sound it takes 10x longer then my version to collect all axis ... (which is not the slow party of the script and could be neglegted - I was just buffled by the profiling i ran)

kind regards
mogh

Hi,

ten times slower sounds rough. It is probably because I was a bit lazy and used a lookup table (`visited`) to traverse the scene-graph, which is a rather expensive thing to do. Feel free to use the fixed traversal if performance becomes an issue here.

Cheers,
Ferdinand