GSoC 2017 Dev Log: jnxd

Update 9th July 2017

Just made the PR as described in my previous post.

Supported methods (all booleans, extrude, mirror, makeFillet2, makeChamfer2) now come with an optional withHistory parameter that can be set to True if you want to store history. The development of the sub-shapes from the sub-shapes of the base(s) can be studied by using shapeName.History.modified, shapeName.History.generated, shapeName.History.isDeleted.

Some testing is required, however. Works well enough for my test cases, but they are fairly naive. Some stress-testing would be greatly appreciated.

Thanks for the PR, jnxd. I hope it get merged soon. What else have you been working on ?

Hi, @Kunda1. Glad to see your interest in the development.

Unfortunately, most of my time went in development towards the PR. It doesn’t really look like much, but the commits are a result of squashing around 35 commits, and are the result of the GSoC work done so far. My next efforts are towards getting rid of the need of storing the entire BRepBuilderAPI_MakeShape.

I was tending towards using OCC’s TNaming framework, but it seems I might have to change TopoHistory’s methods for using it. Strictly going by experiments using the framework as a black-box, it seems we will also need a context parameter, which is the shape that the sub-shape we’re searching for is a part of, for the solution to work. I imagine this won’t be much of a problem, but I am wondering why this is the case.

I did read @ezzieguywuf’s post, but I haven’t yet read through his code. As of now, I think I will continue using OCC’s tools, but @DeepSOIC and I have been regularly arguing whether developing something of our own would be a better idea. From @ezzieguywuf’s post, it does seem that OCC has some poorly documented code. Also, since FreeCAD itself is a framework, I can imagine stacking it on top of OCC’s OCAF (that TNaming is a part of) would be less efficient. Still, I think it should turn out relatively straightforward to change from OCC’s to our own implementation when it is ready.

Is there not a way to ask by OpenCascade if they have some more/ better info around there to fix?
Or else Salome use the same kernel is there not a way to see how they fix some things??

@jnxd, I was messing with this a bit today and noticed an issue. Upon further investigation, it seems that you have a type on Line 72 of TopoHistory.cpp. I believe this should call shapeMaker->Generated not shapeMaker->Modified. I’ve made this change to my local copy and everything works as expected.

Great work on this by the way!

Update 27th July 2017

With a PR complete, my efforts since the last update had been towards developing a simple implementation that gives sensible toponaming for a small set of Part::Features that would be useful. I chose Box, Boolean and Fillet because they had a way to define all faces (Box has Top, Bottom, Left, Right, Front, and Back, the others have it based on which faces/edges of the previous shapes(s) they were modified/generated from). For some shapes, like Cone, it appears OCC simply does not have a method to identify all sub shapes (for cone, only the lateral face)

In my previous post (on 14th), I had said I plan to use an implementation of TopoHistory that avoids storing BRepBuilderAPI_MakeShape, and I was using TNaming for that. Unfortunately, the approach suffers from 2 problems:

  1. We can only track modified subshapes. For example, in a prism P generated from a face F, if I feed in an edge E of F into the selector, Tnaming_Selector::Solve succeeds but only returns E as a part of P. Since there is a method to store generation data into the framework, I suspect there might be some way to retrieve it too, but so far I don’t know much about it.
  2. We lose all BRepBuilderAPI_MakeShape specific data, like front, back, etc., which is useful in comparing subshapes between the shapes of a feature before and after modification (thanks @ickby for pointing it out). For this a per-feature reimplementation of BRepBuilderAPI_MakeShape might be necessary to both keep these data, and avoid the huge memory penalty of BRepBuilderAPI_MakeShape.

However, TNaming has a major advantage over BRepBuilderAPI_MakeShape in that it can be created between shapes not necessarily sharing a input-output relationship. We can, thus, store a translated cube as a modification of the original cube. Thus, I now plan to postpone the plan of re-implementing TopoHistory, but create a separate class TopoParaHistory that uses TNaming to relate modifications of the same feature.

Once that is decided, next is how to do the toponaming proper. For that, I had shared an implementation for an idealized model, and I’m sharing it again:
design_notes.txt (4.75 KB)
The horizontal (and diagonal) arrows within the plan would be stored as TopoHistory and the vertical ones will be stored as TopoParaHistory. I planned to complete the TopoNaming solely using the method Part::Feature::execute and it’s redefinitions in the subclasses, as so:

App::DocumentObjectExecReturn *Feature::execute(void)
{
// 1. From para-history of dependencies modify parameters if needed.

// 2. See if something wrong has happened after modification

// 3. Perform the main execute

// 4. Create para-history for this feature
}

Unfortunately this makes an incorrect assumption that execute will only be called once, and that a recompute will call execute of all the shapes when initiated. However, this is not the case. For instance, when editing a sketch, the method has to be called multiple times as we add, remove and modify the various elements of the sketch. Or, it is called multiple times when we modify the parameters of any feature, like the length of a cube. Thus, something like this fails:

>>> # Make Cube (using Button)
>>> oB = App.ActiveDocument.Box.Shape
>>> # Edit length of box from combo-view (say 10 mm -> 5 mm)
>>> nB = App.ActiveDocument.Box.Shape
>>> nB.ParaHistory.modified(oB.Faces[0], oB)

Efforts are underway to figure out how to make a para-history only when the editing between the shape before the editing starts, and that after it finishes. Any inputs as to how this can be achieved will be greatly appreciated.

The developments discussed here can be found on branch tnaming-3 of my fork.

I ran into this when I was working on my tnaming proof of concept last year. It has been a while, but I believe I found a workaround. When I have a few mi items I’ll look through my code to see how I dealt with it.

That is an interesting approach… Actually a big number of parametric objects in FreeCAD could do that: define faces when they create their shape, instead of letting OCC do it automatically…

I wonder how that would go about. For one, the algorithms of OCC will define these faces, we can only choose to ignore it. But as for having our own implementation, we are kind of in the dark. DeepSOIC suggested we use the Face numbers, but it’s going to be hell for cases where there are multiple faces. A cone in OCC and FC, for example, is a term used to span cones, frustums (frusta?) and slices of cones or frustums. Thus, a cone object can have 2, 3, 4 or 5 faces! Adding to that that we’ll have to code the para-histories: 16 combinations!

I agree that relying on hard-coding which face is which in a given Shape will become difficult. I used your same approach, @jnxd, for my TNaming proof-of-concept, but I’m not sure how easily it will really extend.

My current working concept is the following (and can be seen in my PyTopoNamer proof-of-concept): rather than relying on knowing which face is which, the topological namer (I’ll refer to this as TopoNamer in the rest of this post) must only know about when it is created and when its faces are modified. I believe that this information gives TopoNamer enough information to fully track what’s going on. I’ll give an example:

class SolidTopoNamer{
    public:
        SolidTopoNamer(TopoDS_Solid aSolid);
        void addFace(TopoDS_Face aFace);
        void modifyFace(TopoDS_Face oldFace, TopoDS_Face newFace);
        void deleteFace(std::string faceName);
        
    private:
        // This method will create a unique name
        std::string makeName() const;
        TopoDS_Solid mySolid;
        std::map<std::string, TopoDS_Face> myFaces;
        int i;
};

And the implementation might look something like this (this is untested, it probable won’t compile. Just for conversation):

SolidTopoNamer::SolidTopoNamer(TopodDS_Solid aSolid){
    i = 0;
    std::vector<TopoDS_Face> faces = HelperClass.getFaces(aSolid);
    for (auto&& face : faces){
        this->addFace(face);
    }

void Solid::addFace(TopoDS_Face aFace){
    // check to make sure this Face hasn't already been added. throw an error if it has
    std::string name = this->makeName();
    myFaces.insert(std::pair<std::string, TopoDS_Face>(name, aFace));
}

void Solid::modifyFace(TopodS_Face oldFace, TopoDS_Face newFace){
    // loop through myFaces to find the faceName of oldFace. Note that since we throw an error in 
    // addFace if a face is added more than once, this is guaranteed to result in a single key
    oldFaceName = FindOldFaceName();
    myFaces[oldFaceName] = newFace;
}

std::string getName(){
    std::ostream stream;
    stream << "Face" << i;
    i++;
}
}

And finally, a use-case for this might look something like this:

int main(){
    TopoShape box = GetBoxFromSomewhere();
    TopoShape cyl  = GetCylinderFromSomewhere();
    
    SolidTopoNamer namedBox(box);
    
    TopoShape fusedBox = box.Fuse(cyl, withHistory=true)
    
    // excuse this pseudocode
    for (face in box.getFaces()){
        TopoDS_Face newFace = fusedBox.modified(face);
        namedBox.modifyFace(face, newFace);
    }
   // also do namedBox.addFace() for new faces, etc...
}

So you can see in this example, it doesn’t really matter which TopoDS_Face on the Box is the front, back, etc… all that matters is that we keep track of what happens to each Face. I think this approach is much more flexible and should allow for a better long-term solution within FreeCAD.

The idea I had when reading you post is having the two systems in parallel: the existing Face1, Face2, etc… and another system maybe like ezzieyguywuf suggests, a map that contains faces referenced by names such as TopFace, BottomFace, etc. Both would refer to the same face.

Of course this would only work for very controllable objects like a box. But imagine an extrusion could also easily define a BaseFace, SideFace1,2… and and EndFace. Then a fillet between two named faces could still be named (some FilletBetweenTopandLeft). An Arch wall always knows what is its “base” face or footprint, etc, etc.

This is a bit hackish and unperfect system, because there would be no guarantee that names are always available for all the faces of an object. So it would be hard to rely on it. It would also quickly become very complicated and unuseful (FilletBetweenFilletOfFilletBetweenAFaceAndAnotherFace, whatever form it would take) But, it would have the advantage of distributing the responsibility of correct naming to each FreeCAD feature, and it’s also implementable step by step, since it doesn’t replace the current system. And it’s a “viral” system, it could spread over FreeCAD and get bettered over time…

Not a very usable idea in itself, but maybe it can give you further ideas..

@yorik, i"m not sure if I follow your logic here. Or perhaps I have done a poor job explaining how the system I’ve described (and prototyped in Python) works. It seems that when you refer to a “Topological Name”, you are looking for a descriptive name that let’s you find the given topological entity. In a very simple case of a Cube, a given Edge would be named “EdgeOfTopAndFrontFace” in the system you’ve described.

However, naming Edges does not need to be so complicated: indeed, the name of the Edge does not need to have a description of the faces that make it up. Rather, any given Edge simply needs to know the "name"s of the two faces that make it up.

Let’s go back to my example from earlier:

SolidTopoNamer::SolidTopoNamer(TopodDS_Solid aSolid){
    i = 0;
    std::vector<TopoDS_Face> faces = HelperClass.getFaces(aSolid);
    for (auto&& face : faces){
        this->addFace(face);
    }

Notice in the constructor for SolidTopoNamer, there is no mention of a “top” face, “bottom” face, etc. Rather, a simple list of all the faces is taken and given a unique name - “Face1” through “Face6” in this case, based on how the getName method works. This solid could just as easily have been a 10-sided shape (dodecahedron?), it would not have made a difference, all that matters is that we give each face a name.

Now, suppose you want to fillet the Edge between the “top” and “front” face. That’s fine, all you need to remember is that you have filleted “the Edge between Face1 and Face2”.

So, in this simple SolidTopoNamer example, there might be a method ‘SolidTopoNamer::nameEdge(TopoDS_Edge anEdge);’ which similarly names edges as requested. the “nameEdge” method might look something like

void SolidNamer::nameEdge(TopoDS_Edge anEdge){
    std::vector<std::string> parentFaceNames;
    // Again, excuse the pseudocode.
    for (face in mySolid.faceNames){
        for (edge in face.Edges){
            if (edge.IsSame(anEdge)){
                parentFaceNames.push_back(face.getName())
            }
        }
    }
    // edgeNames is a key-value list where each value is a pair of FaceNames. In
    // python, it'd be a dictionary that looks something like this:
    // edgeNames = {"Edge001": ['Face1', Face2'],
    //              "Edge002": ["Face1', "Face3"],etc..>}
    this->addEdge(parentFaceNames);
}

TopoDS_Edge SolidNamer::getEdge(std::string edgeName){
    // first, find the two faces that make up this edge
    std::pair<std::string> parentFaceNames = GetValuesFormEdgeNamesDataStructure();

    // Then, it is a simple matter of finding the one Edge that these two edges
    // have in common
    for (edge1 in face1.edges){
        for (edge2 in face2.edges){
            if (edge1.IsSame(edge2)){
                return edge1;
            }
        }
    }
}

Does this make my approach a bit more clear? Again, it’s not really important where the entity you want a name for is, all that really matters is that you know how to find it. Also, this is why it is import for SolidNamer to have methods such as ModifyFace and AddFace. If one of its named Faces changes, it needs to know so that the next time getEdge is called it is checking the correct object.

This isn’t totally correct. Here’s an example: a shape with two faces and two edges. Good luck figuring out, which edge is the edge between Face1 and Face2. Because both are.
toponaming-from-faces-problem.png
toponaming-from-faces-problem.FCStd (7.69 KB)
For most practical purposes, it’ll probably work; we have to live with this idea, because OCC algorithms only give out the information about face inheritance (for solids).

I am not really sure what I am looking at here. Where are the two faces? Where are the two edges? Can either of these edges be filleted or chamfered? Or are they simply seam edges? If they are seam edges, then why would you need a reference to it?

Please excuse my ignorance. I obviously do not have as much experience with these things as some of you veterans here.

There is a model attached.

Does it matter? Fillet and chamfer are not the only thing which wants an edge. An external geometry in sketcher needs one, and assembly constraint needs one…

They are not seam edges.

Ah, sorry I was viewing on my phone I missed that. This is actually a really great example - as you’ve pointed out, the basic approach I’ve outlined would stumble here. But then again, the approach I’ve outlined is intended to solve the naming problem for primitive solids and their derivatives, thus I believe that expecting to have two face per given edge is reasonable.

That’s not to say that my naming framework is useless in this ‘sweep’ situation. In fact, I believe that my framework would still work - in the case of your revolved sketch an additional “revolved shape manager” (i’ll call it) will need to be created. The job of this RevolvedShapeManager will be to keep track of generated/modified/deleted Faces in the same way that some of the occ helper classes do.

For example, in the case of this revolved shape, the RevolvedShapeManager will need to keep track of the edges that make up the sketch that is revolved, the revolve axis, and the degree of revolution. If any of these changes, RevolvedShapeManager will have to provide a list of modified/added/deleted faces.

In your particular example, RevolvedShapeManager can go one step further: since the degree of revolution is 180 and the revolve-axis is coincident with an edge on the sketch, it can treat the two edges that you mention as a single edge. It only makes sense to do this when these two conditions are met, otherwise the two edges really are two distinct edges.

So, once the RevolvedShapeManager as been written, we can go back to using the regular ol’ SolidNamer as I outlined earlier.

But, my plan is to take this one step at a time. Start with the primitive solids and their derivatives (fuse, etc..), move on to extruded sketches, and finally go on to revolved and swept sketches.

Yes, I was just using these “descriptive names” as a kind of pseudo thing to try to explain the idea better, not as what would really happen inside. In any case, don’t give this too much thought, it’s just a superficial idea that popped out, you guys are much deeper into the subject than me…

Ok, so here is one of my idea about this problem… Unfortunately it is just an brainstorming idea, so I have no demo or code for it and I am not even sure if it can be implemented or if it would work at all. It generally has nothing to do with toponaming but it is possible that if this idea would even work, that it would not work for all cases (not a generic solution, but it would be nice if it would be :slight_smile: ) so maybe it could provide a better (stronger) solution together with toponaming. So lets try to imagine it…

I have tried to make a few steps back and look at this problem from a different perspective. That is, when such an issue happens, the user knows it went wrong because he/she sees that it looks wrong. Lets say a fillet was on edge6 and because of some modification to the object that edge6 moves to an unexpected place and the fillet goes with it. From the simple logic, nothing is really wrong with this, fillet just stayed on edge6 as it was originally set to. But from the user perspective it is wrong because it is not what he/she wants, or so to say, it is not behaving as expected. For some time I did not really move much further from this point and so I was trying to just follow a bit the development around toponaming… Until I got this idea, what if “the expected behavior” in such cases is simply that our example fillet should simply prefer not to move. What do I mean by this, instead of trying to reconstruct and find the right edge with the help from toponaming, what if our fillet (or any other feature or object that suffers from this problem) would prefer to behave is such way that it would move with its edge as long as this changes would come from “expected” transformations to its base object such as move, rotate, resize,… But when unexpected changes would come (eg change in the number of edges in its base object), then the preference of the fillet would be to just stay at its last known position and it would then be willing to “snap” to ANY new edge that would be created in the same position or it would go and complain to the user that it should tell it where to snap to or delete it. I guess the implementation would have to be that the fillet wold probably have to be aware of its position, orientation and shape (eg how long it is) and it would have to be willing to snap to any new edge that is similar enough to its old one. With other words, I guess we can say that instead of names this would rely on position/shape.

So, if this could work, it could potentially also be quite powerful, since it should be possible for the developer to for example set how different the position, shape,… of the new edge can be from the old one for the fillet to still be willing to automatically snap to it instead of complaining to the user to make a decision. And I am guessing that it should be possible to work well even in such cases where several new edges would show up in the same position as the old edge so the fillet could be able to decide to fillet them all…

:question: :neutral_face:

Edit: here is also an idea for a funny prof of concept demo…

In a normal way this would behave as I have described above, so that it would respect its base object and the normal transformations of it like move, rotate,… But lets imagine for fun to ignore even this normal actions, so we create a Cube and add a Fillet to it, now since our Fillet is, as described in the above idea, respecting and holding on to its position in the coordinate system and since in this example we are ignoring even the movement of its base object it means that the Fillet should fail (disappear) if we just move the Cube, but and this is the fun part, if we move the Cube back to the original position the Fillet should snap back to it and show up. And if we now ignore even the base object, then it means that if we create the same Cube and Fillet as before and now move Cube away (Fillet fails and disappears) and we then create a second Cube001 and move it to the same position as the original Cube was, the fillet should snap to and show up on Cube001. Now this is probably not how we will want it to work in a normal way but I was thinking it is a fun and nice example of the basic idea :slight_smile:

Work Product

Before I put up the work product, I must begin the work product with a disclosure. Due to my movement to the US in the middle of the work period and the ensuing visa restrictions, I had to decline a part of the stipend (the third installment). This part was supposed to be corresponding to the final 4 weeks of the period, a time when I have not been able to stay sufficiently involved with the project. This was due to the preparation for the movement, and later the movement itself. I must sincerely apologize to the FreeCAD community for this, and I do plan to continue developing for FreeCAD as I can make time.

That, being said, following is the summary of my contribution during the work period of Google Summer of Code 2017:

The project was towards adding a topological naming framework for FreeCAD, such that topological features of a 3D object, such as it’s vertices, edges and faces, can be described in a stable manner invarant of any modifications in the creation history of the object.

My work:

  • The project plan was to develop a framework based on the following design notes. To summarize, the implementation would keep track of the development of various topological features along the history of the object. When any parameter earlier on in the history is changed, a “para-history” between the old shape at that point in history and its modified shape is generated, which is used to modify the next consecutive shape. Again, another “para-history” can be built for this shape, and so on, until an error occurs, where one can either call for an exception, or leave it to the user to decide what to do.
    design_notes.txt (4.75 KB)
  • I created a class TopoHistory, which is used to describe the developments along the history of the object. At least until the PR, it is effectively a wrapper for OCC’s BRepBuilderAPI_MakeShape, and is limited in use to features that use a single instance of that class for their development. I also created a TopoParaHistory class that was intended to be used to encode the relationship between the topological elements of a shape and it’s modification.
  • A pull request was made after the TopoHistory class was implemented and used within a few Part::Feature sub-classes.
  • The development of TopoParaHistory and of an independent implementation of TopoHistory not storing BRepBuilderAPI_MakeShapes will continue in a separate branch tnaming-3.
  • Apart from this, there were various small tests I did to see how OCC’s TNaming framework works under various cases. They are all consolidated in a single file here.
    fillet_with_cut_testing.zip (7.8 KB)

Future:

The project is definitely far from over. I had initially hoped to at least reach a point where a classical problem of filleting an edge/edges of a box with a slot can be solved, but technical and other difficulties prevented that from happening. This goal still stays a stepping stone towards complete topological naming. Some of the major issues towards this goal are:

  • Where to create the various TopoHistory/TopoParaHistory objects needed to be stored? Initially the idea was to do so in Part::Feature::execute, but it turns out that that method is called multiple times on a single shape before it is called on subsequent shapes, making it harder to find a relation between the old shape and the new.
  • TopoParahistory relies on the TNaming framework, which, unfortunately, only seems to be working well for modifications, but not for generations or deletions.
  • Apart from this there are some more fundamental challenges. One example is that we are so far using just the modification of faces in a solid to track the modification of all edges/vertices. But what happens when there are multiple edges in common between the same face(s)?

To summarize the project has helped me with understanding the problems faced during topological naming, and also gave me an experience with collaboration in a FOSS software. Despite it’s failure, I am optimistic that I would be able to develop this project further.

Do you have the feeling, that this can be solved after all? I mean a sound, always applicable technique. I can only think of some heuristics to solve this problem, because for us humans it is most of the time easy to see which edges, vertices and faces correspond if we have a state before and after a change, but this is very hard to do by a machine.
Example: if I take something like a simple cube and cut it in two pieces of different size; which face should get the same number as before? Which edges are the same?