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();
Code: Select all
linkTest()
- 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)
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.
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