Starting from version 0.3, support of stable topological naming of geometry element has been added to my FreeCAD Link branch. Because Assembly3 is the main test environment, the document is put here.
Update: Version 0.4 now has full support of element mapping in all features
from the Part
workbench. In addition, element mapping is fully supported in
Python. Most python features automatically gain the benefit of element mapping
without any code modification.
Current FreeCAD uses generic type
+ index
as the topological names of
various geometry elements, such as Face1
, Edge2
, and Vertex3
. The indexing
order of the elements is determined by the CAD kernel,
OCCT, which is consistent
during document save and restore. However, when the model is edited, i.e.
when new geometry elements are added or removed, the element indices are
rearranged/reused, and there is no easy way of tracking which one is which after
the modification.
There has been other attempts trying to solve this problem, e.g. here, here, and here And various research papers posted in the forum, such as this. They all served as great inspirations leading to the solution presented here, which is offered as a general framework that is
-
fully backward compatible, meaning that you can save your model in new version of FreeCAD and still able to open it in older version of the software. You will loose all new style topological naming of the model, obviously. But your link properties continues to work; and,
-
automatic, with very little code modification of existing workbench, because the framework resides in the core, and other workbench get the benefit through various property links which are enhanced. When you open models created in older version of FreeCAD, it automatically gains the benefit of the new topological naming feature after recompute.
The framework deals with abstract geometry element name mapping, and name
reference auto update. The core does not know or care how the topological names
are generated and mapped. It provides necessary ground works for other modules
to generate stable topological names. At the time of this writing,
Sketcher.SketchObject
can now provide a complete stable topological naming,
and both Part
and PartDesign
have full implementation of the topological
name generation.
In the current FreeCAD core, the interface class that describes a geometry shape
is called ComplexGeoData
. It has been extended to include an optional
ElementMap
, with the following new APIs in both C++ and Python,
-
setElementName(element,name, postfix=None, overwrite=False)
. It is used to assign a newname
to anelement
with optional postfix, e.g.setElementName('Edge1','e1')
. An element can have multiple names, but a name can only be associated to one element. You can overwrite existing mapping by theoverwrite
parameter. The purpose of postfix is to make it easy for the caller to form meaningful names, such as use the postfix to identify operation, and mark modifications. More importantly, as the name gets longer,ComplexGeoData
allows the caller to compress the name using string hash, but it will ensure that the postfix remains the same, for more advanced applications like deducing modified elements, etc. More details about string hashing will be given in later section. -
getElementName(name,reverse=False)
. Returns the original element name by the givenname
, or, the other way around ifreverse
is true.
The entire element map is also exposed to C++ with set/getElementMap()
, and
as writable attribute called ElementMap
in Python.
When exposed to Gui.Selection
, ComplexGeoData
uses a special prefix to mark
the mapped element. Currently, ;
is chosen as the prefix. When calling
setElementName()
, the special prefix is optional, and will be stripped away before
storage, same for getElementName()
. More details about the involvement of
Gui.Selection
will be given later.
ComplexGeoData
does not know or care the content stored in the map. It is up
to the inherited geometry classes to provide the semantic meaning of the
content. These APIs also make it possible for other core classes to work
their magic to provide seamless upgrade. Here is how it works,
GeoFeature
, the core geometry document object class, now provides a few
convenient method (getElementName()
, resolveElements()
) to let other query
the element map in its associated PropertyComplexGeoData
. After GeoFeature
detects any element name changes in its onChanged()
method, it will signal all
its dependent objects to update their geometry element references.
PropertyLinkSub
, PropertyLinkSubList
, and PropertyXLink
are the only
properties in the core that can carry geometry element reference information.
Other objects that use those properties are free to choose which type of element
name to store as reference, either the original or the mapped one. Behind the
scene, they will keep a shadow copy of both the original and mapped element
reference (if there is one) each time they are assigned a new reference. When
updating, if and only if there exists a shadow copy of the mapped element
reference, will it auto updates its original index based element reference.
Let's take an example, say, a compound with a mapped reference F1 -> Face1
.
When the user assigns the reference Face1
to a PropertyLinkSub
, it will
immediately query the element map, and stores a shadow copy of both F1
and
Face1
. When you updates a compound, say rearrange the order of its children,
its element name will surely change. Assume the compound has updated the mapping
for F1
to Face10
. When PropertyLinkSub
is asked to update its reference,
it will query the current element mapping for F1
, and obtained the updated
reference Face10
, and then update the user assigned value from Face1
to
Face10
automatically.
The reason why we keeps a shadow copy of the unmapped reference is because the
user is free to choose to store the new mapped name as well. Say, it stores F1
in the above example, then no user visible update will happen. But
PropertyLinkSub
will still keep the shadow copy of the unmapped reference up
to date behind the scene. When saving, PropertyLinkSub
will store this
up-to-date unmapped reference just like before, so that the old version FreeCAD
can still open the document, and every links inside works as expected.
PropertyLinkSub
will also save the user assigned value, if and only if it is
a new mapped name, so that when restoring in the new FreeCAD, it stays the same.
The old version FreeCAD will simply skip it.
You may be wondering why can't we just hide the whole mapped element reference
name behind the scene, and only assign the original element name to
PropertyLinkSub
, and let the core works its magic to keep it up-to-date.
There are cases where you'll prefer a constant reference name. For example,
The new sketch export feature allows you to
export any private geometry element of a sketch, including the external
edges. Sketch will use the new mapped reference name for linking to external
edges, and then use the SHA1 hash of the link reference as the element name for
export. The stability of the mapped name ensures the stability of the export,
as well.
When you call Gui.Selection.getSelectionEx()
, it will return the geometry
element selection in the SubElementNames
attribute of its returned value. My
Link
branch FreeCAD went through major upgrade to bring you full object
hierarchy information through this SubElementName
field, as a .
separated
string. For example, when you selects a face of a PartDesign
Pad
feature,
SubElementNames
will give you something like, Pad.Face1
. Now, it is further
extended to include any mapped element name found, as well, like, Pad.;F1.Face
.
The ;
is used here to mark the following name as a mapped element name instead
of an object name. The mapped element name is not obtained by Gui.Selection
directly, but by the object's view object. Just like the current FreeCAD relying
on the view object's getElement()
function to return the selected element name
Face1
, it now returns ;F1.Face1
if the mapping is found. As a result, when
you hover your mouse over some geometry element in the 3D view, you will now see
the new mapped element reference in status bar.
By default, calling Gui.Selection.getSelectionEx()
will automatically walk
down the selected object hierarchy, and resolve the final object, and its
selected geometry element index based reference, so that any existing workbench
sees exactly what they expect. New code can pass an additional parameter
resolve
, set to 2
to see the mapped reference name, or 0
to see the
entire object hierarchy. That's how a PropertyLinkSub
can be assigned
a mapped geometry reference.
It may seems that we have yet to enter the main topic, considering the fact that
the title of this article is called Topological Naming, and we haven't
actually talked about it. However, let me assure you the utter importance of the
ground works described above. Because, only when you actually tries to implement
a full working solution, will you realize that it is now difficult to implement
some seemingly straight forward operations, like copy and paste. Because,
ironically, in order to have a stable topological naming, it has to include
object identification. But copy and paste generates new objects, with new
identification, and thus, will completely change all existing topological
naming. In other word, it is useless to copy topological names. We have to rely
on old style type
+index
to re-generate all mappings, and re-establish all
property links. Without the framework above, it just won't work.
Now, we are going to talk about the actual topological name generation, which I consider to be fairly standard, most of which follows some of the research papers out there.
Each document object in FreeCAD will now be assigned an integer identification number that is unique within its owner document. UUID is not really useful here, when objects can be freely copied and pasted from other documents, which may be a copy by themselves. There is no uniqueness guarantee outside of the object's document. A simple integer is simpler, cheaper, much easier to manipulate.
Part.TopoShape
has a new public member variable called Tag
that is used to
link a shape to its owner object. When the shape is generated by code, the user
is free to assign any integer value to Tag
. The rule is that, zero Tag
disables
automatic element mapping. When mapping an element from another shape with a
different Tag, it will append the Tag into the mapped element name.
TopoShape
now offers a helper method to map the same geometry element from
other TopoShape
instance into itself.
mapSubElement(shape, op=None)
op
is an optional string to insert before the mapped element name.
In Python, TopoShape
's sub-shape attributes will all use mapSubElement
to
transfer the element mapping from the owner shape to its sub-shapes. For
example, run the following code in FreeCAD Python console,
import Part,pprint
cube = Part.makeBox(10,10,10)
cube.Tag = 1
cube.ElementMap
{}
pprint.pprint(cube.Face2.ElementMap)
{'Edge5': 'Edge1',
'Edge6': 'Edge2',
'Edge7': 'Edge3',
'Edge8': 'Edge4',
'Face2': 'Face1',
'Vertex5': 'Vertex1',
'Vertex6': 'Vertex2',
'Vertex7': 'Vertex3',
'Vertex8': 'Vertex4'}
As you can see, cube itself does not have any element mapping, because it is a
primitive shape, and thus, all its element can be considered as fixed. To
enable sub-shape element mapping, set the Tag
to non-zero, and you can see how
sub-shape cube.Face2
maps the original cube's element name into its own
elements. In the returned dictionary, the key is cube's original element name,
and the value is the sub-shape's element name. The element mapping will be
generated by all sub-shape accessors, like Solids
, Wires
, etc.
Although the Part
primitive shape does not have element map by default, the
user (or Python code) can assign customized names that are persistent, so can
the generic Part::Feature
object. Be careful though, if you change the element
name of a feature, all its dependent feature may change their element name, too,
which may invalidates the element reference inside some link properties. You
should choose a fixed rule to generate element names in your derived classes.
import Part
doc = App.newDocument('test')
cube = doc.addObject('Part::Box','cube')
shape = cube.Shape.copy()
shape.setElementName('Face1','F1')
cube.Shape = shape
doc.recompute()
doc.save('test.fcstd')
App.closeDocument('test')
doc = App.openDocument('test.fcstd')
doc.getObject('cube').Shape.ElementMap
{'F1': 'Face1'}
TopoShape
offers several new maker
method that utilize this element
mapping. In C++, all of the new maker
function name starts with makE
, such
as makECompound
, makEWires
, etc. All new maker code is
implemented in a new source file called TopoShapeEx.cpp
. In Python, however,
the changes are made by change the C++ code of existing method, so that
existing Python feature can get the benefit without any python code
modification.
There two categories of maker functions. The first category of makers do not modify any existing elements. Instead, it generates new elements by simply combining input lower level elements. They are,
makECompound
, exposed to Python using bothPart.makeCompound
andPart.Compound
constructor.makEWires
, exposed to Python as a new method inPart.Shape
namedmakeWire
, and also asPart.Wire
constructor. This function accepts list or compound of unsorted edges. And will automatically connects the edges to form one or more wires (as compound)makECompSolid
, exposed to Python asPart.CompSolid
constructor
All of these method do not really generate new element names, but only map existing element names, although, it may append the tags of the input shape to avoid name conflict.
The following code demonstrate how makECompound
maps the input shape elements
into the new compound shape. For name Edge10;:T1:6
, the trailing ;:T
is the
marker for tag. The following 1
is the actual tag, and :6
is the source
element name length. 6
means the first five characters, i.e. Edge10
.
import Part,pprint
cube = Part.makeBox(10,10,10)
cube.Tag = 1
cube2 = Part.makeBox(10,10,10,App.Vector(10,0,0))
cube2.Tag = 2
compound = Part.makeCompound([cube,cube2])
pprint.pprint(compound.ElementMap)
{'Edge10;:T1:6': 'Edge10',
'Edge10;:T2:6': 'Edge22',
'Edge11;:T1:6': 'Edge11',
'Edge11;:T2:6': 'Edge23',
...
'Vertex7;:T2:7': 'Vertex15',
'Vertex8;:T1:7': 'Vertex8',
'Vertex8;:T2:7': 'Vertex16'}
The Sketcher now uses TopoShape::makEWires
to create its public geometry
wires and map its internal geometry element names. Too see the effect, create
any sketch, and type the following command in the console.
pprint.pprint(App.ActiveDocument.Sketch.Shape.ElementMap)
{'g1;SKT': 'Edge1',
'g1v1;SKT': 'Vertex1',
'g1v2;SKT': 'Vertex2',
...
}
The second category of maker relies on OCCT
's
BRepBuilderAPI_MakeShape
and its derivatives to construct the shape. And TopoShape
relies on this
class to provide the shape history information, specifically, the mapping from
the input geometry elements to the newly generated or modified elements in the
resulting shape. The details of the mapping algorithm can be found [[here|Topological-Naming-Algorithm]]
As you will find out after reading the [[Topological Naming Algorithm]], the
generated element names are going to be very long.
And as we add more histories to the model, the length of a new element name will
quickly go out of control. To deal with this problem, a new object is introduced,
StringHasher
. It allows you to transform any string into an integer ID.
SthringHasher.Threshold
controls how the strings are stored. If Threshold
is
positive, and the string length goes beyond this value, the original string
content will be discarded, and only persists the crypto hash of the string,
currently SHA1. If Threshold
is non-positive, then it will keep the string as
it is. The returned integer ID is guaranteed to be unique within its owner
StringHasher
object.
Every Document
now has a default StringHasher
object accessible through
Document.Hasher
. ComplexGeoData
also has an attribute named Hasher
which
is initially None
. Simply assign the Document.Hasher
to it, or, if you want,
create a new one by App.StringHasher()
. ComplexGeoData.setElementName()
will encode the mapped element name if there is a Hasher
available.
TopoShape
inherits the Hasher
from ComplexGeoData
. When you assign
a TopoShape
containing a Hasher
to a PropertyPartShape
, the hasher will be
persisted to the document. The string inside the Hasher
is reference counted.
By default, only used string will be persisted. You can change that behavior by
setting attribute StringHasher.SaveAll
to True
. ComplexGeoData
will only
hash the element name if there is at least a separator ;
found in the middle
of the name. TopoShape
will auto append this separator when mapping element
from other shapes with a non zero tag.
To see the effect of the string hasher,
import Part,pprint
cube = Part.makeBox(10,10,10)
cube.Tag = 1
cube.Hasher = App.StringHasher()
cube.Hasher.Threshold=10
cube.setElementName('Edge1','A_LONG_ELEMENT;NAME')
'#1'
cube.setElementName('Edge2','SHORT;NAME')
'#2'
pprint.pprint(cube.ElementMap)
{'#1': 'Edge1', '#2': 'Edge2'}
pprint.pprint(compound.Hasher.Table)
{1: '724be94858dd7faa13ce515d46fc6849616b1a44', 2: 'SHORT;NAME'}
compound = Part.makeCompound(cube)
pprint.pprint(compound.ElementMap)
{'#1;:T1:2': 'Edge1',
'#2;:T1:2': 'Edge2',
'Edge10;:T1:6': 'Edge10',
'Edge11;:T1:6': 'Edge11'
...
}
As you can see, the first element name exceeds the threshold, so Hasher.Table
stores the SHA1 hash of the assigned element name. To use the StringHasher
more effectively, you should use StringHasher.getID()
for both string lookup
or ID creation. The returned value is a StringID
object that is reference
counted.
hasher = App.StringHasher()
id = hasher.getID("abcde")
# Note that the number after '#' is a hex number
print str(id)
'#1'
print id.Value
1
print id.Data
abcde
id2 = hasher.getID(1)
print id.isSame(id2)
True
Each document now has a new property named UseHasher
to control be hashing
behavior. Part
workbench features will check this property to decide
whether to hash the generated element names. It is by default on. When you
manually set this property to False
, the document will touch every geometry
feature inside, and you'll need to recompute the entire document to get the
updated element map.
The default StringHasher.Threshold
is zero, meaning that all string will be
stored in the table without crypto hashing. This is because the original string
content is important to trace back modeling history. Tracing back history is
not need to maintain a stable element reference, however, it is useful in other
applications, such as view provider element color mapping.
There is one more important details that is handled inside ComplexGeoData
and TopoShape
. Most of the shape building involves multiple steps. To make it
maker API user to use, we do not demand only hashing in the very last modeling
step, which means that it is possible to hash more than once from the input
shape element name to the final result shape element mapped names.
However, if we hash element names in the intermediate steps, where the
shapes produced are not persisted, it is possible to have lost the string
ID when restoring the document, causing unwanted element name change. To
prevent this, each entry in the element map inside ComplexGeoData
will
keep an array of every historically used string ID references that
are involved in generating that map entry. And the arrays are persisted along
with the element map. However, tests shows that the persisted form of this
string IDs often gets quite long, often longer than the element name itself.
Future development may explore the possibility to reduce file size by simply
set Hasher.SaveAll
to True
, and discard all the intermediate string ID
references.
As you can see from the description so far, as well as the naming
[[algorithm|Topological-Naming-Algorithm#algorithm]], the element name generation is
a very delicate process. It can be affected by many factors, such as whether
the user turns on the hasher, the hasher threshold, how the feature assigns the
primary element name, the TopoShape
name generating algorithm, and of course,
OCCT's
maker's mapping logic that TopoShape
relies on to obtain the shape
history. Any change of these factors may result in significant changes in the
generated element names, and can potentially break existing element references.
To prepare for this possibility, we add a new API to GeoFeature
to report its
element map version. It returns a string that is persistent. Derived features
can choose to override it to store its own version. In fact, it is very important
for any geometry feature to change the version number if there is any change in
its shape building logic. The default implementation will return something as
below,
1.4.70100.1.40
| | | | |
| | | | --If use hasher, then this is the hasher threshold
| | | |
| | | --ComplexGeoData algorithm version, now is 1
| | |
| | --OCCT version
| |
| --TopoShape algorithm version, now is 4
|
--1 means using the same hasher as the document's, or else 0
Any geometry feature can simply append their own version (if any) behind this.
When a document is restored, PropertyPartShape
will compare the persisted
version string with the current one, and mark the feature for recompute
if it is changed. It will also inform any link property that has element
reference to this feature to reset its shadow copies, similar to what
happens when an geometry feature object is copied and pasted.