[Microstation CE u13] ElementGraphicsProcessor example in C#

Hi all,

I am trying to extract all of the elements contained within a paramteric cell, and it was suggested that I use the ElementGraphicsProcessor abstract class to achieve this. The objective is to to return all graphical elements from the cell instance in their true location (with the same geometry), along with their associated symbology. Essentially this would have a similar outcome as dropping the cell however the typical workflow for dropping an element programmatically doesn't seem to work on parametric cells unfortunately. Does anyone have any examples or documentation that could enlighten me on how to achieve this?

Thanks!


Ed

Parents
  • Hi Edward,

    and it was suggested that I use the ElementGraphicsProcessor abstract class to achieve this

    Er ... no, in fact. I recommend to use ElementGraphicsProcessor to obtain geometry, not elements! It's important to distinguish between these two different information ;-)

    The objective is to to return all graphical elements from the cell instance in their true location (with the same geometry)

    In general, some objects allow to access internal elements, some not. There is even a method called ExposeChildren to determine whether this functionality is supported. My feeling is that general concept of new API is to do not expose internal elements (as elements), because it's implementation / persistence detail, but to prefer to use the object geometry everywhere it's possible.

    Whereas e.g. plain cell is the example of the element exposing children, parametric cell is black bock and it's not possible to access original elements (at lest in the current API version). What is not clear whether it's "by design" or it's just "not implemented yet" feature, because constraints-based modeling is still under development and new features are added.

    the typical workflow for dropping an element programmatically doesn't seem to work on parametric cells unfortunately

    I think the drop tool does not work well with parametric cells in general, not only in API.

    Does anyone have any examples or documentation that could enlighten me on how to achieve this?

    There is, the processor is used in some example delivered with SDK and when you will search this forum, you will find discussion about different aspects of this (great and clever!) object ... also because it seems in some situations the wrapper around native processor does not work as expected.

    The code can be similar to this one:

        internal class Demo
        {
            public void IterateAllElements()
            {
                ParametricCellProcessor processor = new ParametricCellProcessor();
    
                DgnModel model = Session.Instance.GetActiveDgnModel();
    
                ModelElementsCollection elements = model.GetGraphicElements();
    
                foreach (Element element in elements)
                {
                    if (element is ParametricCellElement instance)
                    {
                        MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
                        ElementGraphicsOutput.Process(instance, processor);
                    }
                }
            }
        }
    
        internal class ParametricCellProcessor : ElementGraphicsProcessor
        {
            public override bool ProcessAsBody(bool isCurved)
            {
                return false;
            }
    
            public override bool ProcessAsFacets(bool isPolyface)
            {
                return false;
            }
    
            public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled)
            {
                return BentleyStatus.Error;
            }
    
            public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
            {
                MessageCenter.Instance.StatusMessage = $"{curve.GetCurvePrimitiveType()}";
                return BentleyStatus.Success;
            }
    
            public override bool WantClipping()
            {
                return false;
            }
        }

    I recommend (in fact, I think you have to) to read ElementGraphicsProcessor description in native MicroStatioAPI doc, because it's crucial to understand what "bool returning" methods should be overridden to obtain what result.

    When you are interested in symbology (or other "not geometry info), override proper AnnounceXYZ method.

    With regards,

      Jan

  • Thanks Jan,

    use ElementGraphicsProcessor to obtain geometry, not elements!

    You're right, poor choice of words! I need to obtain the geometry of the parametric cell in order to build the (new) elements.

    In general, some objects allow to access internal elements, some not. There is even a method called ExposeChildren to determine whether this functionality is supported

    This is actually what I attempted at first, but quickly realised that there were no children that could be exposed within a Parametric Cell Element. If only it were that easy!

    I think the drop tool does not work well with parametric cells in general, not only in API

    If attempting to do this manually using the 'Drop Element' tool with just the 'Application Elements' ticked everything works as I'd hoped it would, dropping them to their original elements:

    Which makes me wonder if perhaps their is a simpler method to drop Application Elements to their primitives/breps?

    I recommend (in fact, I think you have to) to read ElementGraphicsProcessor description in native MicroStatioAPI doc, because it's crucial to understand what "bool returning" methods should be overridden to obtain what result

    Thanks for the code sample, I'll test that out. With regards to the documentation; when reading through the API docs and forum posts, I didn't quite understand how the geometry is returned to the main script. Am I right in assuming I should read the geometry output from the MessageCenter and use that info to build all the required geometry, through the use of Bentley.GeometryNET library? Looking through the GeometryNET library there are a vast array of methods for each element type; and it looks like a very arduous task if I need to write a script to use the correct methods for drawing an arc, line etc. for all the possible permutations that may occur! Hopefully this is not the case and my interpretation is wrong... What seemed like an easy task in order to finish my GC Node has turned into a full day of struggling to get nowhere lol.
    I really appreciate all the advice to help make sense of this!

    Thanks,

    Ed

  • If only it were that easy!

    Maybe easy for developers, but wrong in terms of data structures.

    It's my subjective feeling, but I see a movement from "C data structures" to "object abstraction using handlers (services)". Whereas it was normal in DGN V7 and also in V8 times to go inside structures and to change directly different parts (which often ended in strange or even corrupted elements), now it's more like "here is a handler, call services (queries) that you need, but do not care about implementation details".

    As written in C++ documentation, every handler should be very careful about providing a list of children elements, because in such case the handler has not any control over them. So for some objects it's possible, for some not.

    If attempting to do this manually using the 'Drop Element' tool with just the 'Application Elements' ticked everything works as I'd hoped it would, dropping them to their original elements:

    I guess there is no problem with 2D constraints, but I tried it with some 3D objects and the results were bad.

    Which makes me wonder if perhaps their is a simpler method to drop Application Elements to their primitives/breps?

    There is Drop structure available in C++, but I think it's not available in managed API (yet?).

    I didn't quite understand how the geometry is returned to the main script.

    I am not sure what you are asking about.

    What processor method is called and how (in what format) the geometry is provided depends on the processor configuration (what type of geometry are you interested in).

    What you will do next with received geometry is completely up to you (e.g. list of geometries added to a list injected to processor?) and does not relate to API itself.

    Looking through the GeometryNET library

    Why? At first, look at examples delivered with MicroStation SDK, at second, search in this forum, at third, read this community blogs (even when there is not many articles). In my opinion, after several years of CE API existence, all typical activities / workflows have been discussed and snippets provided.

    and use that info to build all the required geometry, through the use of Bentley.GeometryNET library?

    Not at all. GeometryNET library is mostly internal aspect of API and usually there is no reason to use it for anything (only to reference the assembly when required).

    To create elements from existing topology / geometry data, use DraftinElementSchema class.

    and it looks like a very arduous task if I need to write a script to use the correct methods for drawing an arc, line etc. for all the possible permutations that may occur!

    If your code ends with such state, something is probably wrong and it means you do not use OOP abstraction provided by API properly. For many requirements, specialized (single focused) class probably exists in API.

    What seemed like an easy task in order to finish my GC Node has turned into a full day of struggling to get nowhere lol.

    Sorry, but such comparison is complete nonsense in my opinion. There is no reason compare platform MicroStation NET API and specialized GC environment. They are different tools developed for different purposes.

    GC were implemented using platform API and it's why the platform API exists: Namely C++ API is ready to solve nearly any requirement and allows to implement nearly anything including all other discipline-specific product APIs. The low-level and flexible API typically requires to write more code ... similarly when you will try to compare C++ (powerful but seriously complicated) and Python (more user oriented and not so flexible, but more friendly).

    With regards,

      Jan

Reply
  • If only it were that easy!

    Maybe easy for developers, but wrong in terms of data structures.

    It's my subjective feeling, but I see a movement from "C data structures" to "object abstraction using handlers (services)". Whereas it was normal in DGN V7 and also in V8 times to go inside structures and to change directly different parts (which often ended in strange or even corrupted elements), now it's more like "here is a handler, call services (queries) that you need, but do not care about implementation details".

    As written in C++ documentation, every handler should be very careful about providing a list of children elements, because in such case the handler has not any control over them. So for some objects it's possible, for some not.

    If attempting to do this manually using the 'Drop Element' tool with just the 'Application Elements' ticked everything works as I'd hoped it would, dropping them to their original elements:

    I guess there is no problem with 2D constraints, but I tried it with some 3D objects and the results were bad.

    Which makes me wonder if perhaps their is a simpler method to drop Application Elements to their primitives/breps?

    There is Drop structure available in C++, but I think it's not available in managed API (yet?).

    I didn't quite understand how the geometry is returned to the main script.

    I am not sure what you are asking about.

    What processor method is called and how (in what format) the geometry is provided depends on the processor configuration (what type of geometry are you interested in).

    What you will do next with received geometry is completely up to you (e.g. list of geometries added to a list injected to processor?) and does not relate to API itself.

    Looking through the GeometryNET library

    Why? At first, look at examples delivered with MicroStation SDK, at second, search in this forum, at third, read this community blogs (even when there is not many articles). In my opinion, after several years of CE API existence, all typical activities / workflows have been discussed and snippets provided.

    and use that info to build all the required geometry, through the use of Bentley.GeometryNET library?

    Not at all. GeometryNET library is mostly internal aspect of API and usually there is no reason to use it for anything (only to reference the assembly when required).

    To create elements from existing topology / geometry data, use DraftinElementSchema class.

    and it looks like a very arduous task if I need to write a script to use the correct methods for drawing an arc, line etc. for all the possible permutations that may occur!

    If your code ends with such state, something is probably wrong and it means you do not use OOP abstraction provided by API properly. For many requirements, specialized (single focused) class probably exists in API.

    What seemed like an easy task in order to finish my GC Node has turned into a full day of struggling to get nowhere lol.

    Sorry, but such comparison is complete nonsense in my opinion. There is no reason compare platform MicroStation NET API and specialized GC environment. They are different tools developed for different purposes.

    GC were implemented using platform API and it's why the platform API exists: Namely C++ API is ready to solve nearly any requirement and allows to implement nearly anything including all other discipline-specific product APIs. The low-level and flexible API typically requires to write more code ... similarly when you will try to compare C++ (powerful but seriously complicated) and Python (more user oriented and not so flexible, but more friendly).

    With regards,

      Jan

Children
  • Thanks Jan,

    What you will do next with received geometry is completely up to you (e.g. list of geometries added to a list injected to processor?) and does not relate to API itself.

    Sorry I should have been more specific; I can see that the processor gathers all the geometry information but how do I then enumerate the results / pass this as a variable to DraftingElementSchema.ToElement/s? Please bear in mind I am very new to all of this, I've only had the SDK for a few weeks so I am finding things out the hard way ;)

    Code I have so far:

            public NodeUpdateResult DropParametricCell
            (
                NodeUpdateContext updateContext,
                [Replicatable, DgnModelProvider] object ParametricCell
            )
            {
                try
                {
                    Feature feat = ParametricCell as Feature;
                    long eleRef = feat.GetElement().MdlElementRef(); //Get Element Ref of GC Feature as long
                    Bentley.DgnPlatformNET.Elements.Element ele = Bentley.DgnPlatformNET.Elements.Element.GetFromElementRef((IntPtr)eleRef); //Retrieve Element as DgnPlatformNET Element
    
                    DgnModel model = Session.Instance.GetActiveDgnModel();
                    ParametricCellProcessor processor = new ParametricCellProcessor();
    
                    if(ele is ParametricCellElement instance)
                    {
                        Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
                        ElementGraphicsOutput.Process(instance, processor);
                        //What do I do now?!
                        //DraftingElementSchema.ToElement(model, , null);
                    }
                }
                catch (Exception ex)
                {
                    return new NodeUpdateResult.TechniqueException(ex);
                }
                return NodeUpdateResult.Success;
            }
        } // class
    
        internal class ParametricCellProcessor : ElementGraphicsProcessor
        {
            public override bool ProcessAsBody(bool isCurved)
            {
                return false;
            }
    
            public override bool ProcessAsFacets(bool isPolyface)
            {
                return false;
            }
    
            public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled)
            {
                return BentleyStatus.Error;
            }
    
            public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
            {
                Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"{curve.GetCurvePrimitiveType()}";
                return BentleyStatus.Success;
            }
    
            public override bool WantClipping()
            {
                return false;
            }
        }
    } // namespace

    Thanks for all the help getting me this far!

    Edit - I think I finally understand what is required to pass the geometry to the main script. Do I need to add a List to collect the elements within the processor? The issue now is that I do not know what to pass to the DraftingElementSchema for the template element. Updated code:

            public NodeUpdateResult DropParametricCell
            (
                NodeUpdateContext updateContext,
                [Replicatable, DgnModelProvider] object ParametricCell
            )
            {
                try
                {
                    Feature feat = ParametricCell as Feature;
                    long eleRef = feat.GetElement().MdlElementRef(); //Get Element Ref of GC Feature as long
                    Bentley.DgnPlatformNET.Elements.Element ele = Bentley.DgnPlatformNET.Elements.Element.GetFromElementRef((IntPtr)eleRef); //Retrieve Element as DgnPlatformNET Element
    
                    DgnModel model = Session.Instance.GetActiveDgnModel();
                    ParametricCellProcessor processor = new ParametricCellProcessor();
    
                    if(ele is ParametricCellElement instance)
                    {
                        Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
                        
                        ElementGraphicsOutput.Process(instance, processor);
    
                        for (int i = 0; i < processor.curvePrimitives.Count; ++i)
                        {
                            Bentley.DgnPlatformNET.Elements.Element nele = DraftingElementSchema.ToElement(model, processor.curvePrimitives[i], null); //Currently throws an error...
                            nele.AddToModel();
                            Bentley.Interop.MicroStationDGN.Element fetch = MSApp.ActiveModelReference.GetLastValidGraphicalElement();
                            SetElement(fetch);
                            nele.Dispose();
                        }
                    }
                    
    
                }
                catch (Exception ex)
                {
                    return new NodeUpdateResult.TechniqueException(ex);
                }
    
                return NodeUpdateResult.Success;
            }
        } // class
    
        public class ParametricCellProcessor : ElementGraphicsProcessor
        {
            public List<CurvePrimitive> curvePrimitives = new List<CurvePrimitive>();
    
            public override bool ProcessAsBody(bool isCurved)
            {
                return false;
            }
    
            public override bool ProcessAsFacets(bool isPolyface)
            {
                return false;
            }
    
            public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled)
            {
                return BentleyStatus.Error;
            }
    
            public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
            {
                Bentley.MstnPlatformNET.MessageCenter.Instance.StatusMessage = $"{curve.GetCurvePrimitiveType()}";
                curvePrimitives.Add(curve);
                
                return BentleyStatus.Success;
            }
    
            public override bool WantClipping()
            {
                return false;
            }
        }
    

    It throws an error on line 24, most likely due to null value for template. The list count = 8 though so it seems to be collecting something; when looking through debug values it is a heap of nested lists though and I couldn't see any raw geometry. Might be in there somewhere!

  • Hi Ed,

    Please bear in mind I am very new to all of this

    New to what? One thing is start with MicroStation NET API, which can be confusing, but now much better knowledge base (examples, blogs, discussion) is available than when CE was released. But sometimes my feeling is that for people the problem is not in new API, but in weak knowledge of basic OOP concepts (SOLID etc.).

    but how do I then enumerate the results / pass this as a variable to DraftingElementSchema.

    In my opinion dependency injection is the solution here.

    Do I need to add a List to collect the elements within the processor?

    It's up to you what is your intention and also what software architecture do you prefer (because it can be implemented in several ways). Also, do not forget that Element is IDisposable, so you have to ensure it will be disposed explicitly.

    It throws an error on line 24, most likely due to null value for template.

    Absolutely not! When you say "error", it's like silence. What error? Is an exception thrown? When yes, what exception?

    Updated code:

    There are some ugliness:

    • What fashion (because I do not know about any reason) I have seen in many shared snippets to pollute code with full classes names (including namespaces)?
    • I do not know GC API, so I am not sure what is the best way, but to use ModelRef to obtain element plus using explicit casting (which potentially can ends with casting exception) looks not very elegant.

    This should work (at least MicroStation part), but be aware it's a concept, the structure is not fine (especially how elements are disposed):

    public NodeUpdateResult DropParametricCell
        (
            NodeUpdateContext updateContext,
            [Replicatable, DgnModelProvider] object ParametricCell
        )
        {
            try
            {
                Feature feat = ParametricCell as Feature;
                long id = feat.Element.ID;
    
                DgnModel model = Session.Instance.GetActiveDgnModel();
                Element element = dgnModel.FindElementById(new ElementId(ref id));
                
                ParametricCellProcessor processor = new ParametricCellProcessor();
    
    
                if (element is ParametricCellElement instance)
                {
                    MessageCenter.Instance.StatusMessage = $"Parametric cell: {instance.CellDefinition.CellName}";
    
                    List<Element> extracted = new List<Element>();
    
                    try
                    {
                        ParametricCellProcessor processor = new ParametricCellProcessor(extracted, model);
                        ElementGraphicsOutput.Process(instance, processor);
    
                        foreach (Element el in extracted)
                        {
                            el.AddToModel();
                        }
                    }
                    finally
                    {
                        if (null != extracted)
                        {
                            foreach (Element el in extracted)
                            {
                                el.Dispose();
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                return new NodeUpdateResult.TechniqueException(ex);
            }
    
            return NodeUpdateResult.Success;
            
        }
    } // class
    
    internal class ParametricCellProcessor : ElementGraphicsProcessor
    {
        private readonly IList<Element> elements;
        private readonly DgnModel dgnModel;
        private DTransform3d trans;
    
        private ParametricCellProcessor()
        {
        }
    
        public ParametricCellProcessor(IList<Element> elements, DgnModel dgnModel)
        {
            this.elements = elements ?? throw new ArgumentNullException(nameof(elements));
            this.dgnModel = dgnModel ?? throw new ArgumentNullException(nameof(dgnModel));
        }
    
        public override void AnnounceTransform(DTransform3d trans)
        {
            this.trans = trans;
        }
    
        public override bool ProcessAsBody(bool isCurved) => false;
    
        public override bool ProcessAsFacets(bool isPolyface) => false;
    
        public override BentleyStatus ProcessCurveVector(CurveVector curves, bool isFilled) => BentleyStatus.Error;
    
        public override BentleyStatus ProcessCurvePrimitive(CurvePrimitive curve, bool isClosed, bool isFilled)
        {
            Element element = DraftingElementSchema.ToElement(this.dgnModel, curve, null);
            element.ApplyTransform(new TransformInfo(this.trans));
            this.elements.Add(element);
            return BentleyStatus.Success;
        }
    }

    Regards,

      Jan

    Answer Verified By: Edward Ashbolt 

  • Jan you are legend, it works great! Thanks so much for all your advice and assistance with this, I definitely wouldn't have gotten this far otherwise. Really stoked to finally get that operational! If you ever make it over to this part of the world (Melbourne, Australia) I will be buying you all the beers as thanks ;)
    Cheers,

    Ed