-
-
Notifications
You must be signed in to change notification settings - Fork 74
Topological Naming Algorithm
- Overview
- Algorithm
- Test Example
- Hashing Element Name
- Tracing Model History
- Element Coloring
- Overhead
This article details the actual algorithm of generating the topological names
using the shape modeling history. The history information is provided by OCCT
's
BRepBuilderAPI_MakeShape
and its derivatives. They provides the mapping from the input geometry elements
to the newly generated or modified elements in the resulting shape. However, As
OCCT
being OCCT
, not all of OCCT's
maker classes follow the same interface.
There are exceptions, which is why TopoShape
uses an abstract class
TopoShape::Mapper
as the mapping interface. Special handling is required for
the following OCCT
maker,
-
BRepBuilderAPI_Sewing
(used inmakEOffset
, etc.) -
BRepOffsetAPI_ThruSections
(used inmakELoft
, etc.)
The core function to generate element names is TopoShape::makESHAPE()
. At
a high level, the algorithm can be described as using four steps to try to name
as many elements as possible.
- Find any unchanged and named elements that appear in both the input and out shape, and just copy those names over to the new shape.
- Assign names for generated and modified elements by querying the
Mapper
. - For all unnamed elements, assign an indexed based name using its upper named
element. For example, if a face is named
F1
, its unnamed edges will be named asF1;:U1
,F1;:U2
, etc. In this step, an element may be assigned multiple names, if it appear in more than one named upper elements. This is to improve the name stability of the next step. - For all remaining unnamed elements, use the combination of lower named
elements as its name. For example, if a face has all its edges named, then
the face will be named as something like
(edge1,edge2,edge3);:L
.
A simplified version of the algorithm is presented with the following pseudo Python code,
def makESHAPE(result_shape, mapper, input_shapes, opcode):
# first, map any unchanged elements into the result shape
for shape in input_shapes:
result_shape.mapSubElement(shape)
# collect names from input shapes that generates or modifies elements in
# the result shape
# a map of map to store the names
names = defaultdict(dict)
for element_type in ('Vertex','Edge','Face'):
for shape in input_shapes:
for i,sub_shape in enumerate(shape.getSubShapes(element_type)):
# get the mapped element name of this sub shape. 'True' here
# means we are asking for reverse mapping, i.e. query the mapped
# name using the index name
mapped_name = str(shape.Tag) + '_' + \
shape.getElementName(element_type+str(i+1),True)
# get all the new_sub_shape that is modified from sub_shape
for k,new_sub_shape in enumerate(mapper.getModified(sub_shape)):
# get the topological indexed names of this new sub shape,
# i.e. Vertex1, Edge2, Face3, etc.
index_name = result_shape.getIndexName(new_sub_shape)
# stores a map entry from new_element_index ->
# input_mapped_element_name Also stores the input shape's
# Tag, and the order index for disambiguation, because a new
# element may be modified from more than one input elements
names[index_name][mapped_name] = k+1
# now, do the same for new_sub_shape that are generated from
# sub_shape
for k,new_sub_shape in enumerate(mapper.getGenerated(sub_shape)):
index_name = result_shape.getIndexName(new_sub_shape)
# we use a negative order index to indicate its a generated
# element
names[index_name][mapped_name] = -(k+1)
# Construct names from the information collected in the above steps
for index_name,name_map in names.items():
postfix=None
for n,(mapped_name,index) in enumerate(name_map.items()):
if n==0:
# special treatment of the first mapped element name from the
# input shape. We shall use this as prefix for advanced
# application, e.g. to find all shapes that are generated or
# modified from a common source.
first_tag = mapped_name.split('_')[0]
first_name= mapped_name[len(first_tag)+1:]
first_index = index
name_type = 'M' if index>0 else 'G'
continue
# name type tells us if this name is for a modified (='M') or
# generated (='G') shape. Although it shouldn't happen, but we are
# prepared to accept a shape that is both generated and modified
# (='MG')
if (name_type=='M' and index<0) or \
(name_type=='G' and index>0):
name_type = 'MG'
# throw all the rest of names into a pair of brackets
if n==1:
postfix = '('
else:
postfix += ','
postfix+= mapped_name + ':' + str(index)
if postfix:
postfix += ')'
first_index = abs(first_index)
postfix = ';:{}{}{};{};:T{}:{}:{}'.format(name_type,
first_index,
postfix,
opcode,
first_tag,
len(first_name),
index_name[0])
result_shape.setElementName(index_name, name=first_name,postfix=postfix)
# Suppose Face1 of result shape is generated by Edge2 of shape tag 1,
# and Edge3 of shape tag 2, and Edge4 of shape tag 3, with opcode 'XTR',
# it will be named as
#
# Edge2;:G1(2_Edge3:2,3_Edge4:2);XTR;:T1:4:F
#
# Marker ; is used as separator for different components of the name.
# Special marker ;: is reserved by TopoShape to mark any internal used
# components, to distinguish with user supplied op code.
#
# ;:G indicates the element is generated. The following 1 is the
# generation index. Because any source elements may generate more than
# one shape, the index is here for disambiguation. In the actual
# algorithm, we omit any index of '1' to reduce some verbosity.
#
# The 2 in 2_Edge3 is the owner shape tag of Edge3.
#
# The :2 in 2_Edge3:2 is the generation index of shape tag 2 Edge3.
#
# The ending ;:T1 marks the tag (1 in this case) of the first source
# shape, and the following :4 is the length of the first source element
# name. And the next :F marks the element type of this name, to make
# sure we don't lost element type information if the element is gone due
# to model changes. This ending mark, starting with ;:T, is very
# important for tracing back element evolving history.
# Now, the reverse pass. Starting from the highest level element, i.e.
# Face, for any element that are named, assign names for its lower unnamed
# elements. For example, if Edge1 is named E1, and its vertices are not
# named, then name them as E1;:U1, E1;:U2, etc.
#
# In order to make the name as stable as possible, we may assign multiple
# names (which must be sorted, because we may use the first one to name
# upper element in the final pass) to lower element if it appears in
# multiple higher elements, e.g. same edge in multiple faces.
names.clear()
for upper_type,element_type in (('Face','Edge'),('Edge','Vertex')):
for i,sub_shape in enumerate(result_shape.getSubShapes(upper_type)):
# get the mapped element name of the new sub shape. 'True' here
# means we are asking for reverse mapping, i.e. query the mapped
# name using the index name.
index_name = upper_type+str(i+1)
upper_mapped_name = result_shape.getElementName(index_name,True)
# skip any non-mapped names
if upper_mapped_name == index_name:
continue
n=0
for element_shape in sub_shape.getSubShapes(element_type):
# get the indexed name of this lower element in the result
# shape
index_name = result_shape.getIndexName(element_shape)
# skip any named elements
if result_shape.getElementName(index_name,True) != index_name:
continue;
n = n+1
names[index_name][upper_mapped_name] = n
# assign the actual names
for index_name,name_map in names:
for upper_mapped_name,index in name_map.items():
postfix = ';:U{};{}'.format(index,opcode)
# In case the upper name has a trailing ;:T marker, extract the
# source tag
upper_tag = getTagFromName(upper_mapped_name)
if upper_tag:
postfix += ';:T{}:{}:{}'.format(
upper_tag,len(upper_mapped_name),index_name[0])
result_shape.setElementName(
index_name,
name = upper_mapped_name,
postfix = postfix)
# Finally, the forward pass. For any elements that are not named, try
# construct its name from the lower elements
for element_type,lower_type in (('Edge','Vertex'),('Face','Edge')):
for i,sub_shape in enumerate(result_shape.getSubShapes(element_type)):
index_name = element_type + str(i+1)
# skip any named element
if result_shape.getElementName(index_name, True) != index_name:
continue
names = set()
for element_shape in sub_shape.getSubShapes(lower_type):
# get the indexed name of this lower element in the result
# shape
index_name = result_shape.getIndexName(element_shape)
mapped_name = result_shape.getElementName(index_name,True)
if mapped_name == index_name:
# only name the upper element if all lower elements are
# named
names.clear()
break
names.insert(mapped_name)
if not names:
continue
# Construct the element name. For example, if opcode is XTR, and a
# face with edges Edge1, Edge2, Edge3 will be named as
# Edge1;:L(Edge2,Edge3);XTR
names = list(names)
first_name = names[0]
postfix = ';:L({});{}'.format(','.join(names[1:]),opcode)
# In case the lower name has a trailing ;:T marker, extract the
# source tag
lower_tag = getTagFromName(first_name)
if lower_tag:
postfix += ';:T{}:{}:{}'.format(
lower_tag,len(first_name),index_name[0])
result_shape.setElementName(index_name,
name = first_name,
postfix = postfix)
Let's take an example to see the new topological naming in action.
We make a fusion with a simple Cube
and Cylinder
. Note that, I have turned
off string hash in advance to show the raw topological names. You can do this
by typing in the console,
App.ActiveDocument.UseHasher = False
See the following section for more details of the string hasher.
You can see that the modified top Cube face is named as
Face6;:M2;FUS;:T1:5:F
| | | | | |
| | | | | -- The type of this element, 'F' for 'Face'
| | | | |
| | | | -- The length of the source element name. This is used to
| | | | trace back the model history. In this case, the first
| | | | five characters gives the original element name of 'Face6'.
| | | |
| | | -- ';:T' is the marker for tag of owner shape of the original
| | | element. The following '1' is the actual tag value. In this
| | | case, it is the Cube's object ID.
| | |
| | -- op code
| |
| --';:M' is the modification maker, and the following '2' is the
| modification index, in case the source element modifies into more
| than one elements.
|
-- The source element name. The length is recorded at the end of the mapped
name. In case a string hasher is used. This will be the hashed name, and
the length is the length of the name after hashing.
The modified top cylinder face is similarly names as Face2;:M2;FUS;:T2:5:F
The middle smaller face is a modification from two input elements, and is named
as Face6;:M(Face2;:T2:5:F);FUS;:T1:5:F
. Ignoring what's inside the bracket, we get
Face6;:M();FUS;:T1:5
, and this is similarly named as the first modified face
originated from the Cube
. The Face2;:T2:5:F
inside the bracket is the second source
name and its tag and name length. In case there are more sources, it will be
included in the bracket separated by ,
.
Next, we turn on model refine.
The top three faces are merged into one face, and its name got a lot longer.
Face2;:M2;FUS;:T2:5:F;:M(Face6;:M(Face2;:T2:5:F);FUS;:T1:5:F,Face6;:M2;FUS;:T1:5:F);RFI;:T2:21:F
This name is difficult for human to read, but easy for program to parse. The ending
;:T2:21:F
tells the tag, the name length of the first source element, and the element type. With that, we
can easily find out the source element name being the first 21 characters,
Face2;:M2;FUS;:T2:5:F
. Now, strip out the first source name and the trailing
tag and length information, we have
;:M(Face6;:M(Face2;:T2:5:F);FUS;:T1:5:F,Face6;:M2;FUS;:T1:5:F);RFI
| | | | | |
| | | | | -- op code, indicates
| | | --------------------- refine operation
| | | |
| ----------------------------------- -- The third source element name
| |
| -- The second source element name
|
-- The modification marker
Next, we create a fillet by selecting an edge,
Edge5;:T1:5:E.Edge5
. The trailing .Edge5
indicates the old style indexed
element name. As you can see, when we turned off Refine
in the fusion, the
edge index jumped from 5 to 9, but its mapped element name is still
Edge5;:T1:5:E
, with 1
being the Cube's object ID, i.e. Edge5
from Cube 1.
To further showcase a more advanced application of the new naming scheme, we add a new cylinder to the fusion in the following screen cast.
The fillet is now broken, because the originally selected edge is gone. Its
name will not be reused like old index based names.You can see from the above
screen cast that the original edge is modified, and its name has changed from
Edge5;:T1:5:E
into Edge5;:M;FUS;:T1:5:E
. TopoShape
offers function called
getRelatedElements()
to help obtaining potential replacement of the missing
references. This is how the fillet editing tool can auto select the related
edge when activated, as shown in the screen cast. The algorithm of
getRelatedElements()
is described below,
def getRelatedElements(shape,name):
names = {}
tag,length,type = getTagInfoFromName(name)
if not tag:
return
# First, try to recover the first source element.
source = name[:length]
# try to recover source name
dehashed = deHash(name[:length])
tag2 = getTagFromName(dehashed)
if not tag2:
# In case the tag number is not in the dehashed name, we
# will append it manually, because when any name is mapped,
# we will always append a tag as shown in the above naming
# algorithm
dehashed += ';:T{}:{}:{}'.format(tag,len(dehashed),type)
# check if there is actually an element with this recovered name
element = shape.getElementName(dehashed)
if element != dehashed:
names[dehashed] = element
# Next, search any element that is modified from the given name.
#
# This step is simplified here as it does not include details of dealing
# with possible hashing.
modNames = shape.getElementWithPrefix(name + ';:M')
names.update(modNames)
# Finally, search any element that are modified from the same source of
# the given name
modNames = shape.getElementWithPrefix(source + ';:M')
names.update(modNames)
return names
The following screen cast shows various ways to mess around the fusion, and the fillet tool can always guess the correct edge.
Pay attention to the first modification, where we move the cylinder into the
middle of the Cube
edge, the Fillet
object seems to moved the fillet edge
downwards and missing the upper one. The reason is because, if an element is
modified into more than one new elements, its name will include a modification
index. The order of this index relies on what OCCT
reports to us. So in this
case, what Fillet
object sees is that the element reference still exists, and
it is the lower edge, because OCCT
reports it as the first. We can arguably
include more information into the naming, e.g. add any existing lower element
names into the modified name, which will be the vertex name in this case. But
then, imaging that the edges got split into more than two segments, the middle
ones still requires the index to disambiguation, and problem still remains.
The fillet feature relies on PropertyLinkSub
to obtain the edge
reference from the mapped element name. If the model changed such that the
mapped element is gone, or in other word, the mapped element name is changed,
the fillet will report an error of invalid edge link. It is the fillet editing
tool that does the guessing by querying various prefixes. This guessing is not
always reliable, and requires user attention, which is why it is not automated
inside fillet feature.
The length of the generated names will be kept manageable by the string hasher. In order to keep as much information as possible, the hashing is performed in multiple segments of the element name, in relation to how names are constructed. For example, the refined face above will be hashed as follow,
Face2;:M2;FUS;:T2:5:F;:M(Face6;:M(Face2;:T2:5:F);FUS;:T1:5:F,Face6;:M2;FUS;:T1:5:F);RFI;:T2:21:F
| | | |
| | -----------------------------------------------------------
--------------------- |
| -- second segment
|
-- first segment, i.e. the name of the first source element
The final hashed name is,
#a8;:M#a7;RFI;:T2:2:F
| | | | | |
| | | | | -- The element type, 'F' for 'Face'
| | | | |
| | | | -- The length is now changed to be the length of the hashed
| | | | name, i.e. '#a8'
| | | |
| | | -- The tag of the first source element stays the same.
| | |
| | -- op code stays the same
| |
| -- Second hash.
|
-- First hash, where 'a8' is a hex number indicating the hashed string ID,
which can be used to recove the original string from the hash table if the
original string length does not exceed hash table threshold.
The naming algorithm is specifically designed to ease the recovery of the
first source element name. TopoShape
offers a function named getElementHistory()
,
in both C++ and Python, to decode the input name and return the first source
element name and tag. Part::Feature
offers a function with the same name that
recursively calls its TopoShape's
getElementHistory()
to give you a linear
history of any given element.
For example, with the above fillet object, to obtain the history of the top refined face,
for o in App.ActiveDocument.Fillet.getElementHistory('Face2'):
print '{} {}'.format(o[0].Name, o[1:])
Fillet ('#21;:M;FLT;:T3:3:F', [])
Fusion ('#8;:M#7;RFI;:T2:2:F', ['Face2;:M2;FUS;:T2:5:F'])
Cylinder ('Face2', [])
The history is returned in a list of
tuple(sourceObject, sourceElementName, list[intermediateSteps...])
The intermediateSteps
are any intermediate modeling steps performed by an
object. As the above naming algorithm shows, each step will append the shape
tag of the first source element. If the object invokes more than one
TopoShaper
new style maker, then the same tag will be appended more than
once. The intermediate modeling steps are recovered by recursively decoding the
element name until the tag changes.
Note that the getRelatedElements()
described in previous section works fine
when guessing missing element names caused by changes in the previous model
step. It works poorly when the change happens several steps into the history.
A more general method is exposed as Part.getRelatedElement()
to combat this
problem. It calls the above TopoShape::getRelatedElements()
first. And if
nothing is returned, it will try to trace each and every elements history, and
return those having the same initial source element name as the missing one.
One very practical application of history tracing is element color mapping. We
know that FreeCAD already has element mapping in boolean Part
features.
However, without a stable element reference, the color mapping is less
meaningful, because you either rely on the algorithm to combine colors from
child objects for you, or choose to override the color of the entire object.
You can set individual face color, but it will get overwritten every time you
do a recompute.
With the new stable element reference, plus the capability of element history
trace, ViewProviderPartExt
, which is the parent view provider of all derived
Part::Feature
now has a generalized implementation of element color mapping,
which are controlled by four new properties, MapFaceColor
, MapLineColor
,
MapPointColor
, and MapTransparency
. By default, only MapFaceColor
is on,
which will map child object's face color into parent, just like current boolean
feature, but better. To change default setting, create a boolean parameter with
the same name under BaseApp/Preferences/Mod/Part
.
To demonstrate its capability, let's pick a pure Python feature, the
Part::Connect, which has
some complex modeling logic. The color mapping code, as well as the element
naming algorithm, are general enough that they work for Connect
without
modifying a single line of its Python code.
Notice how the Fillet
object auto colors the new fillet face with one of its
connected faces. That fillet face is generated from an edge.
ViewProviderPartExt
will trace back to the source object, i.e. the Connect
object, and use the first Face
in the Connect
that contains this edge for
coloring.
In order to maintain backward compatibility, no changes in the view object will touch the document object for recompute. You'll need to manually mark the object for recompute to see the color in effect.
The screen cast above also shows how easy it is to override individual element
color and transparency. The element color is stable across recomputation thanks
to the stable element reference. Unlike how Fillet
object treats the edge
reference, ViewProviderPartExt
will auto deduce related element names in case
any reference is gone. To color any element using Python,
vobj = App.ActiveDocument.Connect.ViewObject
# obtain the existing element color as a dict
colors = vobj.ElementColors
# add some new color
colors['Face3'] = (1.0,0.0,0.0,0.5)
vobj.ElementColors = colors
As you may have expected from the above description, the element naming algorithm is complex, and the number of names generated is potentially big. There bound to have some overhead in terms of both the computation time and file size. Here is a non-formal test result to show the negative impact. Note that the string hasher is turned on for this test.
A model file with 148 objects, with only one root level Part workbench object. The dependency graph is shown below.
Without element map | With element map | |
---|---|---|
Total recomupation time | 40 s | 52 s |
Uncompressed File Size | 17.8 MiB | 22.6 MiB |
Compressed (level 3) size | 2.8 MiB | 3.6 MiB |