PR #876: Link, stage one, context aware selection

Merged, abandoned or rejected pull requests are moved here to clear the main Pull Requests forum.
Post Reply
realthunder
Veteran
Posts: 2190
Joined: Tue Jan 03, 2017 10:55 am

PR #876: Link, stage one, context aware selection

Post by realthunder »

This is the first patch set for introducing the Link concept into FC. There are two more stages (i.e. major patch set), along with a few small patches. The major patch set must be applied in sequence, so the follow ups have to wait till its predecessor's been merged.

What current FC has are PropertyLink and friends, and the aim here is to introduce Link as a Feature (DocumentObject). You can think of Link as the symbolic link in Unix file system, or shortcut in Windows. It enables both geometry data and 3D rendering resource sharing. You can also think it as a pointer (or reference) in software programming, which allows for multi-level indirection and abstraction.

The current patch set is focused on reworking of FC 3D selection. FC uses Coin3D for 3D rendering. Coin3D represents geometry information using tree of nodes, transformation node, material node, lineset, faceset nod, etc. Coin3D supports node sharing, meaning that the same node can be added to different trees to be rendered at a different location and/or with a different material. However, current FC cannot handle node sharing, because its selection framework cannot distinguish between all the same nodes in different context. The net effect is that, if you add one of ViewObject's root node into another object, you can see two object at different placement in the 3D view, but selection/preselection of one object will be mirrored to the other object as well. This patch set fixed that problem, hence the title "context aware selection"

Because the change is entirely in Gui space, I can't think of any automated testing script to be effective. Here is a macro for testing, and also a showcase of the new abilities brought by this patch.

Code: Select all

import FreeCAD
import FreeCADGui

class _LinkGroup:
    def __init__(self,obj):
        obj.Proxy = self
        self.Type = "LinkGroup"
        obj.addProperty("App::PropertyLinkList","Group","Base","")
        obj.addProperty("App::PropertyBoolList","Visibilities","Base","")
        obj.addProperty("App::PropertyMap","Subs","Base","")
        obj.addProperty("App::PropertyPlacement","Placement","Base","")

    def execute(self,obj):
        pass

class _ViewProviderLinkGroup:
    def __init__(self,vobj):
        self.childRoot = None
        self.childMap = None
        self.Object = vobj.Object
        self.subMap = dict()
        vobj.Proxy = self

    def __getstate__(self):
        return None

    def __setstate__(self, _state):
        return None

    def attach(self,vobj):
        from pivy import coin
        #  self.childRoot = coin.SoSeparator()
        self.childRoot = coin.SoType.fromName(
                "SoFCSelectionRoot").createInstance()
        self.childMap = dict()
        vobj.addDisplayMode(self.childRoot,"Default")
        self.Object = vobj.Object

    def getDisplayModes(self,_vobj):
        return ["Default"]

    def getDefaultDisplayMode(self):
        return "Default"

    def setDisplayMode(self,mode):
        return mode

    def updateData(self,obj,prop):
        from pivy import coin
        if not self.childRoot:
            return
        if prop == "Group":
            self.childRoot.removeAllChildren()
            self.childMap.clear()
            vobj = obj.ViewObject
            vis = obj.Visibilities
            for i,o in enumerate(obj.Group):
                vo = o.ViewObject
                root = coin.SoSeparator()
                root.setName(o.Name)
                self.childRoot.addChild(root)
                for node in vo.RootNode.getChildren():
                    if node.isOfType(coin.SoSwitch.getClassTypeId()):
                        switch = coin.SoSwitch()
                        for n in node.getChildren():
                            switch.addChild(n)
                        if i>=len(vis) or vis[i]:
                            switch.whichChild = vo.DefaultMode
                        else:
                            switch.whichChild = -1
                        node = switch
                        self.childMap[o.Name] = [vo,root,switch]
                    root.addChild(node)
            subs = obj.Subs
            for key in subs.keys():
                if not key in self.childMap:
                    subs.pop(key)
            obj.Subs = subs

        elif prop == "Subs":
            vobj = obj.ViewObject
            # reset partial rendering first
            vobj.partialRender()
            subs = []
            self.subMap.clear()
            for key,value in obj.Subs.iteritems():
                vset = set(value.split(','))
                # subMap is to accelerate filtering in getElementPicked
                self.subMap[key] = vset
                subs += ['{}.{}'.format(key,sub) for sub in vset]
            if subs:
                vobj.partialRender(subs)

        elif prop == "Visibilities":
            vis = obj.Visibilities
            for i,o in enumerate(obj.Group):
                root = self.childRoot.getChild(i)
                for node in root.getChildren():
                    if node.isOfType(coin.SoSwitch.getClassTypeId()):
                        if i>=len(vis) or vis[i]:
                            node.whichChild = o.ViewObject.DefaultMode
                        else:
                            node.whichChild = -1

        elif prop == "Placement":
            obj.ViewObject.setTransformation(obj.Placement.toMatrix())

    def claimChildren(self):
        return self.Object.Group

    # map subelement name to coin SoPath and SoDetail
    def getDetailPath(self,subname,path,append):
        if not subname:
            return
        if append:
            vo = self.Object.ViewObject
            path.append(vo.RootNode)
            path.append(vo.SwitchNode)
        path.append(self.childRoot)
        dot = subname.find('.')
        if dot<0:
            name = subname
            nextsub = ""
        else:
            name = subname[:dot]
            nextsub = subname[dot+1:]
        try:
            info = self.childMap[name]
            path.append(info[1])
            path.append(info[2])
            return info[0].getDetailPath(nextsub,path,False)
        except KeyError:
            pass

    # map coin SoPath to subelement name
    def getElementPicked(self,pickPoint):
        path = pickPoint.getPath()
        idx = path.findNode(self.childRoot)
        if idx<0:
            return
        try:
            node = path.getNode(idx+1)
            name = node.getName().getString()
            info = self.childMap[name]
            # obtain the subelement name of the linked object
            sub = info[0].getElementPicked(pickPoint)
            if not sub:
                return

            # now, in case of partial rendering, we do filtering here
            try :
                subs = self.subMap[name]
                if subs and sub.split('.')[0] not in subs:
                    return
            except KeyError:
                pass

            # return the sub element name after inserting the linked object's
            # name
            return '{}.{}'.format(name,sub)

        except KeyError:
            pass


def makeLink(objs,name="LinkGroup"):
    obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython",name)
    _LinkGroup(obj)
    _ViewProviderLinkGroup(obj.ViewObject)
    obj.Group = objs
    return obj

def linkTest():
    import Part
    doc = FreeCAD.newDocument()
    box1 = doc.addObject("Part::Feature","box1")
    box1.Shape = Part.makeBox(10,10,10)
    box2 = doc.addObject("Part::Feature","box2")
    box2.Shape = Part.makeBox(10,10,10)
    box2.Placement.Base.x = 20
    fuse = doc.addObject("Part::MultiFuse","fuse")
    fuse.Shapes = [box1,box2]
    fuse.Placement.Base.y = 20
    box1.ViewObject.Visibility = True
    box2.ViewObject.Visibility = True
    doc.recompute()

    # create a link group containing box1, box2 and fuse
    link = makeLink([box1,box2,fuse],"link")
    # enable partial rendering to to show only Face1 and Face2 of box2
    link.Subs = {'box2':'Face1,Face2'}
    # hide box1
    link.Visibilities = [False]
    link.Placement.Base.z = 20

    # create a second link group containing the previous link group
    link2 = makeLink(link,"link2")
    link2.Placement.Base.z = 20

    doc.recompute()
    FreeCADGui.ActiveDocument.ActiveView.fitAll();

Save the code into your Macro directory, or simply copy and paste to FC console, and run

Code: Select all

linkTest()
The code above implement a feature called LinkGroup that is similar to App::Part. It addresses some of the problems that I have with App::Part (@ickby), using the node sharing capability,
  • An object can be added to more than one group without ambiguity
  • objects that are added to a group exist both in the group's local coordinate system and global coordinate system
  • All other tool feature remains unchanged, and still works with object in the global coordinate system.
  • objects in the group (local coordinate system) have independent visibility setting then their counterpart in the global system. The visibilities property is stored in the Object rather than ViewObject, meaning that the script can be certain about the group 3D representation without loading gui.
  • 3D view selection of the objects shows full qualified SubName to locate the object
  • Hierarchy selection. When repeatedly hitting an sub element in 3D view, the selection will be extended to upper hierarchy, i.e. parent group(s)
linkTest() creates a box1, box2, fuse (of box1 and 2) as testing objects, shown as the bottom tier objects in the following picture. It then creates a LinkGroup called 'link' containing these three objects (middle tier). It hides box1 and enables partial rendering of box2 with Face1 and Face2, which is why you only see two faces of it. Finally, it creates link2 (top tier) containing link, i.e. a link to link. Notice, that when selecting the same object in different context, the Selection View shows different full qualified SubName.

Image

Extra feature that is not related to App::Part,
  • Partial rendering, meaning that you can choose to render only part of the sub-elements (Edges, Faces, etc) of a linked object. The purpose of this feature will become apparent in follow up patches.
  • Material override, You can add coin material node to override the overall material of a linked object. This feature is there, but not shown in the code above. It will be available as a build-in feature in the final patch set.
  • Transparent object now retains transparency after being selected/preselected.
  • Gui.Selection has a new feature called pickedList, which is a byproduct of my work on Link. The picked list in the Selection View allows one to pick multiple hidden sub elements using mouse pointer.
Note that, because of the context aware selection, SoBrepPointSet/EdgeSet/FaceSet no longer have highlightIndex/selectionIndex property, which may break some of the python scripts using them (@m42kus). It can be easily fixed, since now Gui.Selection has a new method called preselect(doc,obj,sub,x,y,z) to let the script do preselection with full qualified SubName. Also, new method enablePickedList(), getPickedList() are added to let script access the picked list, along with a new message type in SelectionObserver PickedListChanged=7.

Image

What's not doable in the current patch set (but already been done in my follow up patches) are,
  • Tree view selection synchronization with 3D view
  • Tree view visibility status synchronization with group's object
Final words. The change introduced in this patch shall not affect any existing FC feature (apart from the SoBrep hightlightIndex mentioned above). On the other hand, the changes are nevertheless significant under the hood. Lots of testing is required. Please try my branch at https://github.com/realthunder/FreeCAD/tree/LinkStage1
Try Assembly3 with my custom build of FreeCAD at here.
And if you'd like to show your support, you can donate through patreon, liberapay, or paypal
ickby
Veteran
Posts: 3116
Joined: Wed Oct 05, 2011 7:36 am

Re: PR #876: Link, stage one, context aware selection

Post by ickby »

Hello,

your provided example does not work. It is not possible to make the links visible.
linktest.png
linktest.png (57.25 KiB) Viewed 3081 times

Code: Select all

OS: Ubuntu 16.04.2 LTS
Word size of OS: 64-bit
Word size of FreeCAD: 64-bit
Version: 0.17.11581 (Git)
Build type: Debug
Branch: LinkStage1
Hash: 7bd501bd49049893b34e5c8ca12e3f81f4097594
Python version: 2.7.12
Qt version: 4.8.7
Coin version: 4.0.0a
OCC version: 6.8.0.oce-0.17
realthunder
Veteran
Posts: 2190
Joined: Tue Jan 03, 2017 10:55 am

Re: PR #876: Link, stage one, context aware selection

Post by realthunder »

As I mentioned in the topic post, the tree view does not work as expected at the moment. What you are seeing in the 3D view are not linked object (i.e. objects in the LinkGroup's local coordinate), those are the original objects in the global coordinate system.

Right now, the tree view can only toggle visibility of the objects in the global CS. You can try it and hide those objects by select them in the tree view. In my follow up patches, the children objects of a LinkGroup in the tree view will correctly reflect and control their status in the local coordinate.

What will happen is that, once a LinkGroup claims the child, it shall hide the child object in the global CS. And the tree view only shows the object's visibility status in the local CS. This mimics the same behavior of the current App::Part. The difference is that, when you select the object in the tree view and create a tool feature base of it, only the object in the global CS will be claimed, no matter what group the object is in. So, when you toggle visibility of the object claimed by the tool (i.e. the child object of the tool in the tree view), you can reveal the object in the global CS. You are free to choose whether to put the tool feature output back into the same group of its children or in another group.

I'll soon create a LinkStage2 branch to show the above behavior.
Try Assembly3 with my custom build of FreeCAD at here.
And if you'd like to show your support, you can donate through patreon, liberapay, or paypal
Post Reply