realthunder / FreeCAD_assembly3

Experimental attempt for the next generation assembly workbench for FreeCAD
GNU General Public License v3.0
880 stars 75 forks source link

subshapebinder python api #193

Open dulouie opened 5 years ago

dulouie commented 5 years ago

How can i add geometry and shapes to subshapebinder via script? I have the last feature of the body feature = body.OutList[0] feature is and then create the binder binder = body.newObject('PartDesign::SubShapeBinder',binder') binder.ViewObject.dropObject(feature) RoportView show me: <ShapeBinder> ViewProviderShapeBinder.cpp(325): invalid selection and the binder has a wrong placement, when i double click the binder he solve the placement.

dulouie commented 5 years ago

if idrop the body object to binder: <ShapeBinder> ViewProviderShapeBinder.cpp(319): cannot resolve relative link

ceremcem commented 5 years ago

This may help: https://github.com/realthunder/FreeCAD_assembly3/issues/89

dulouie commented 5 years ago

Thanks

dulouie commented 5 years ago

Okay these function update the binder:

def updateBinder(binder):
    Gui.Selection.clearSelection()
    Gui.Selection.addSelection(binder)
    binder.ViewObject.doubleClicked()

Besides an binder.addGeometry() methode would be nice.

realthunder commented 5 years ago

The addSelection call is very important to provide the necessary context information. Imaging you have two links to the same body containing a binder. In the tree view, the same binder appears in two branches, so the update result after double click will be different depending on which item you click. If there is only one binder, then your code above works as expected. If the binder appears in multiple places, say there is another Link to the body containing a Binder001, then below is how to establish the correct context.

Gui.Selection.addSelection(Link, 'Binder001.')

The same principle applies for drag and drop. Depending on where you drop the object, the relative link is calculated differently. You can experiment with various operations with your mouse. All the Gui.Selection, drag & drop, and double click calls are logged in Python console just in case you want to script it.

dulouie commented 5 years ago

I have some trouble to get the arguments for addSelection(). With myobject.Parents i can get these arguments but sometimes it is strange. With linked object you get a second list witch is switching. And there is no documentation, so i have to find the logic behind this behavior by myself.

class SelectionPath:
    def resolve(self, obj):
        self.obj = obj
        self.doc =  App.ActiveDocument
        if len(obj.Parents)== 1:
            self.linked = False
            self.root = obj.Parents[0][0]
            self.path = obj.Parents[0][1]
            print("[0]rootObject= " + self.root.Name)
            print("[0]SelectionPath= " + self.path)
        else:
            self.linked = True
            self.root = obj.Parents[0][0]
            self.path = obj.Parents[0][1]
            self.root2 = obj.Parents[1][0]
            self.path2 = obj.Parents[1][1]
            print("[0]rootObject= " + self.root.Name)
            print("[0]SelectionPath= " + self.path)
            print("[1]rootObject= " + self.root2.Name)
            print("[1]SelectionPath= " + self.path2 )
    def select(self):
        Gui.Selection.clearSelection()
        if self.linked:
            Gui.Selection.addSelection(doc.Name,self.root2.Name,self.path2)
        else:
            Gui.Selection.addSelection(doc.Name,self.root.Name,self.path)

Is there any simpler plan to get it? Actually i want to shapebinder all last features of bodys in the document expect the activated body. I wish a function to simply add geometry to the binder like binder.addGeometry(obj) so i've set binder.Support mutable.

support = [(self.root,(self.path))] 
binder.Support= support
realthunder commented 5 years ago

The path is supposed to be a string representing the object hierarchy in the tree view. You can check the Python console to see how it is expressed. For example,

Gui.Selection.addSelection('Unnamed','Assembly','Parts.Box.')

So, all three arguments are supposed to be string, addSelection(docName, objName, subName).

In case you have the document object, then there is a second shorter form, addSelection(obj, subName).

You can directly set binder.Support if you know what you are doing. To turn off read only, run

binder.setPropertyStatus('Support', '-Immutable')
binder.Support = [ (self.root, (self.path)) ]

Assuming the self.path above is a correct subname.

dulouie commented 5 years ago

Okay but i don't understand the order of myobj.Parents output. Sometimes is the App::Link and sometimes the Partdesign::Body object the first list entry. Both feature objects are actually App::Link to an external file. Look at my example assembly. The output from the pad of the baselink is:

[0]rootObject= Body
[0]SelectionPath= Pad.
[1]rootObject= Link
[1]SelectionPath= Pad.

And the pad from link1:

[0]rootObject= Link001
[0]SelectionPath= Pad.
[1]rootObject= Body
[1]SelectionPath= Pad.

As you can see the order has switched. Can you explain me why? assembly.zip

dulouie commented 5 years ago

To reproduce it, you have to open the assembly.fcstd, then close it and all linked documents and open it again. Or do you know an other function to get the subName?

realthunder commented 5 years ago

Oh, now I see. You are using the Parents attribute. Internally it is using a map on the object pointer, so of course the order is going to change on document re-open. If you want a consistent order, just store the result in a dictionary. And since the returned object may come from different documents, you may want to include the document name to resolve duplication.

{ o.Document.Name + '#' + o.Name : (o, sub) for o, sub in obj.Parents }

But I don't think Parents is what you want. Can you elaborate on your intention?

dulouie commented 5 years ago

Basically i want to shapebinder all bodies of the document (the last feature) to my new created body. I can get those body's with App.ActiveDocument.Objects then filter for <body object> or <App::Link object>. And to get the last feature, the first entry of OutListRecursive is the last feature of the body.

And now i have a list with features from different bodys. To add it sequential to shapebinder support the subname is needed. Parents is the only function i found for that. So i have to write a function that resolve the structure of the given objects and translate it into subname strings for the correct binder format.

Or simpler if possible: binder.Support=[referenceobj1, referenceobj2, referenceobj3...]

realthunder commented 5 years ago

If you want the Tip of the body, then you should bind to that body directly without any subname, just in case you want to add feature later on. But if you insist, here is the code,

# This gives all top level (i.e. without parent) bodies and link to bodies
bodies = [ o for o in App.ActiveDocument.Objects 
      if o.getLinkedObject(True).isDerivedFrom('PartDesign::Body') and not o.Parents ]

# prepare the property link info
links = [ (o, (o.getLinkedObject(True).Tip.Name+'.')) for o in bodies ]

# add a new body
body = App.ActiveDocument.addObject('PartDesign::Body', 'Body')

# add binder
binder = body.newObject('PartDesign::SubShapeBinder','Binder')

# Set support
# First, turn off readonly
binder.setPropertyStatus('Support','-Immutable')
binder.Support = links
# Turn on readonly again, optionally
binder.setPropertyStatus('Support','Immutable')
dulouie commented 5 years ago

I've written a small testscript for different cases. And only case three is fully working with relative placement. I guess the context in case two is missing?

import FreeCAD

case = 2
doc = App.ActiveDocument

# This gives all top level (i.e. without parent) bodies and link to bodies
bodies = [ o for o in App.ActiveDocument.Objects 
      if o.getLinkedObject(True).isDerivedFrom('PartDesign::Body') and not o.Parents ]

# add a new body
body = App.ActiveDocument.addObject('PartDesign::Body', 'Body')
#set new placement
pl = FreeCAD.Placement()
pl.move(FreeCAD.Vector(-34,0,29.3))
body.Placement = pl

if case==1:
    # preselection of bodies and then create the binder
    for obj in bodies:
        Gui.Selection.addSelection(obj)
    #create new binder
    binder = body.newObject('PartDesign::SubShapeBinder','Binder')
if case==2:
     #set support via script
    #create new binder
    binder = body.newObject('PartDesign::SubShapeBinder','Binder')
     #set support
    binder.setPropertyStatus('Support','-Immutable')
    binder.Support = bodies
if case==3:
    # select the binder and drop objects
    #create new binder
    binder = body.newObject('PartDesign::SubShapeBinder','Binder')
    # select the binder
    Gui.Selection.clearSelection()
    Gui.Selection.addSelection(doc.Name,body.Name,binder.Name+'.')
    #drop objects
    for obj in bodies:
        binder.ViewObject.dropObject(obj,None,'',[])

louieservo.zip

realthunder commented 5 years ago

I see. Right now SubShapeBinder only support placement adjustment when binding to one parent object. You can bind to multiple sub-objects inside one group parent, e.g. put all those bodies in a App::Part. I'll see if I can lift that restriction.

realthunder commented 5 years ago

I have implemented placement adjustment of multiple support. I have also enhanced SubShapeBinder to store the context, so that it can now auto update itself when its parent has moved (@ceremcem you probably would be interested in this too).

The first screencast here shows that the binder auto update itself when its parent body is moved.

binder-1

The screencast shows a new feature. The Placement of the binder is now user changeable to set an offset from the bound objects. This is only available for newly created binder. Existing binder's Placement is still readonly for compatibility reason.

binder-2

This screencast shows when you need to manually adjust context. The parent body of the binder is now put into a new group. This changes the context of the binder. It cannot update itself when the parent group is moved. To establish a new context, just double click the binder as before. You can do that using script with addSelection and doubleClick, but I think it is not really needed since it can now remember the context and auto update itself.

binder-3

This screencast shows another way to change context by moving the bound object to a group. For this, you'll have to manually reset the binding. Just remember to hold CTRL key before dropping the first object to reset instead of adding more bindings.

binder-4

dulouie commented 5 years ago

Thanks for your effort, i have just refactored my script and all is working fine! My goal is to implement a more matured design-in-context feature, based on subshapebinder, App::Link and mutiple document support. At the current state it shows very clear the real potential of App::Link.

ceremcem commented 5 years ago

@realthunder Auto-updating the subshape binder works very well! Thanks! (Wow, I'm a little bit late)

luzpaz commented 3 years ago

Any progress @dulouie ?