MicroStation Python: Interactive Placement Tool


Welcome to the world of MicroStation Python! In our previous wikis, we created basic, complex geometric elements at fixed, known locations. In this wiki, we will explore how to create an interactive placement tool in MicroStation using Python.

 

Create Interactive Placement Tool in MicroStation with Python

Let us dive into, creating an interactive placement tool. Refer to the Python Manager wiki to create and load a python project. Name your project as “PlaceALine.py” and save it to your preferred directory. Open the project in the editor for writing your Python script.

 

Import Modules

The script begins by importing several modules offering functions and classes to interact and create geometric elements with MicroStation.

 

from MSPyBentley import *
from MSPyBentleyGeom import *
from MSPyECObjects import *
from MSPyDgnPlatform import *
from MSPyDgnView import *
from MSPyMstnPlatform import *
import math

 

DgnPrimitiveTool: Construct and Install

The DgnPrimitiveTool class can be used to implement a primitive command. Placement tools that don't need to locate or modify elements are good candidates for a DgnPrimitiveTool.

The __init__(self, toolName, toolPrompt) constructor function initializes the class with a tool name and prompt, calls the base class constructor to ensure proper initialization, sets up an array to store 3D points, and keeps a reference to the instance itself.

toolName: String ID for the tool name is not used in Python, instead _GetToolName is implemented.

toolPrompt: String ID for the initial prompt is not used in Python instead NotificationManager is used to output prompts.

The _OnPostInstall(self) function enables snapping and output prompts.

 

class LineCreator(DgnPrimitiveTool):
    def __init__(self, toolName, toolPrompt):
        # C++ base's __init__ must be called.
        DgnPrimitiveTool.__init__(self, toolName, toolPrompt) 
        self.m_points = DPoint3dArray()
        # Keep self reference
        self.m_self = self 


    def _GetToolName (self, name):
        s = WString ("LineCreator")
        return s


    def _OnPostInstall(self):
        # Enable snapping for create tools.
        AccuSnap.GetInstance().EnableSnap(True) 
        self.SetupAndPromptForNextAction()
        DgnPrimitiveTool._OnPostInstall(self)

 

The InstallNewInstance function is called to construct and install the tool.

 

    def InstallNewInstance(toolId, toolPrompt):   
        tool = LineCreator(toolId, toolPrompt) 
        tool.InstallTool()

 

 

DgnPrimitiveTool: Dynamics

The _OnDynamicFrame (self, ev) function responds to a mouse move event. The parameter DgnButtonEvent ev is the button event which contains the button position/point.

This function implements the RedrawElems API to dynamically/temporarily draw the preview of the Line,  Dimension Text elements.

 

    def _OnDynamicFrame(self, ev):
        tmpPts = DPoint3dArray(self.m_points)
        eeh = EditElementHandle()
        t_eeh = EditElementHandle()

        endPt = ev.GetPoint()

        tmpPts.append(endPt)

        if not self.CreateElement(eeh, tmpPts):
            return
        
        if not self.CreateDimensionTextElement(t_eeh, tmpPts):
            return

        redrawElems = RedrawElems()
        # Display in all views, draws to cursor view first...
        redrawElems.SetDynamicsViews(IViewManager.GetActiveViewSet(), 
                                     ev.GetViewport()) 
        redrawElems.SetDrawMode(eDRAW_MODE_TempDraw)
        redrawElems.SetDrawPurpose(DrawPurpose.eDynamics)

        redrawElems.DoRedraw(eeh)
        redrawElems.DoRedraw(t_eeh)

 

 

DgnPrimitiveTool: Data Button

The _OnDataButton(self, ev) function responds to a mouse left button click. The parameter DgnButtonEvent ev is the button event which contains the button position/point.

This function starts dynamics on first point, saves the data point location and sets up output prompts.

 

    def _OnDataButton(self, ev):
        if len(self.m_points) == 0:
            # Start dynamics on first point. Enables AccuDraw and 
            # triggers _OnDynamicFrame being called.
            self._BeginDynamics() 

        # Save current data point location.
        self.m_points.append(ev.GetPoint()) 
        self.SetupAndPromptForNextAction()

        if len(self.m_points) < 2:
            return False

        endPt = ev.GetPoint()
        self.m_points[1] = endPt

        eeh = EditElementHandle()
        t_eeh = EditElementHandle()

        if self.CreateElement(eeh, self.m_points):
            # Add new line element to active model.
            eeh.AddToModel() 

        if self.CreateDimensionTextElement(t_eeh, self.m_points):
            # Add new line element to active model.
            t_eeh.AddToModel()

        self.m_points.clear()
        # Start of next line is end of current line.
        self.m_points.append(endPt) 

        # Tool should exit after creating a 
        # single line if started in single-shot mode.
        return self._CheckSingleShot()

 

When a minimum of two points are available a Line element is created along with Dimension Text (length of the line) and both the elements are added to the Model.

 

    def CreateElement(self, eeh, points):
        if len(points) != 2:
            return False

        # NOTE: Easier to always work with CurveVector/CurvePrimitive and 
        # not worry about element specific create methods, 
        # ex. LineHandler::CreateLineElement.
        ACTIVEMODEL = ISessionMgr.ActiveDgnModelRef
        if BentleyStatus.eSUCCESS != DraftingElementSchema.ToElement(eeh, 
                                     ICurvePrimitive.CreateLine(DSegment3d(points[0], points[-1])), 
                                     None, ACTIVEMODEL.Is3d(), ACTIVEMODEL):
            return False

        ElementPropertyUtils.ApplyActiveSettings(eeh)

        return True


    def CreateDimensionTextElement(self, t_eeh, points):
        if len(points) != 2:
            return False
        
        ACTIVEMODEL = ISessionMgr.ActiveDgnModelRef
        dgn_file  = ACTIVEMODEL.GetDgnFile()
        dgn_model = ACTIVEMODEL.GetDgnModel()
        model_info = dgn_model.GetModelInfo() 
        mu = model_info.GetUorPerStorage()

        tbProp = TextBlockProperties.Create(dgn_model)
        pProp = ParagraphProperties.Create(dgn_model)
        txStyle = DgnTextStyle.GetSettings(dgn_file)
        rProp = RunProperties.Create(txStyle, dgn_model)
        tb = TextBlock(tbProp, pProp, rProp, dgn_model)

        seg = DSegment3d(points[0], points[-1])
        len_str = str(round(seg.Length()/mu, 2))
        tb.AppendText(len_str)

        midPt = DPoint3d ((points[0].x + points[-1].x)/2, (points[0].y + points[-1].y)/2)
        tb.SetUserOrigin(midPt)

        rmat = RotMatrix ()
        rmat.InitIdentity()
        dAng = math.atan2 (points [-1].y - points [0].y, points [-1].x - points [0].x)
        rmat = RotMatrix.FromVectorAndRotationAngle(DVec3d.From(0,0,1), dAng)
        tb.SetOrientation(rmat)

        status = TextElemHandler.CreateElement(t_eeh, None, tb)
        if TextBlockToElementResult.eTEXTBLOCK_TO_ELEMENT_RESULT_Success != status:
            return False

        ElementPropertyUtils.ApplyActiveSettings(t_eeh)
        
        return True

 

DgnPrimitiveTool: Reset Button

The _OnResetButton (self, ev) function responds to a mouse right button click. The parameter DgnButtonEvent ev is the button event which contains the button position/point.

This function calls the _OnRestartTool function to terminate the current tool session and start a new tool session.

 

    def _OnResetButton(self, ev):
        self._OnRestartTool()
        return True

    def _OnRestartTool(self):
        LineCreator.InstallNewInstance(self.GetToolId(), self.GetToolPrompt())

 

 

Putting it all Together: The main Function

In the main function, the LineCreator class is inherited from DgnPrimitiveTool class and the InstallNewInstance function is called.

 

Here is the complete script.

 

from MSPyBentley import *
from MSPyBentleyGeom import *
from MSPyECObjects import *
from MSPyDgnPlatform import *
from MSPyDgnView import *
from MSPyMstnPlatform import *
import math


class LineCreator(DgnPrimitiveTool):
    def __init__(self, toolName, toolPrompt):
        # C++ base's __init__ must be called.
        DgnPrimitiveTool.__init__(self, toolName, toolPrompt) 
        self.m_points = DPoint3dArray()
        # Keep self reference
        self.m_self = self 


    def _GetToolName (self, name):
        s = WString ("LineCreator")
        return s


    def _OnPostInstall(self):
        # Enable snapping for create tools.
        AccuSnap.GetInstance().EnableSnap(True) 
        self.SetupAndPromptForNextAction()
        DgnPrimitiveTool._OnPostInstall(self)


    def _OnRestartTool(self):
        LineCreator.InstallNewInstance(self.GetToolId(), self.GetToolPrompt())


    def _OnDataButton(self, ev):
        if len(self.m_points) == 0:
            # Start dynamics on first point. Enables AccuDraw and 
            # triggers _OnDynamicFrame being called.
            self._BeginDynamics() 

        # Save current data point location.
        self.m_points.append(ev.GetPoint()) 
        self.SetupAndPromptForNextAction()

        if len(self.m_points) < 2:
            return False

        endPt = ev.GetPoint()
        self.m_points[1] = endPt

        eeh = EditElementHandle()
        t_eeh = EditElementHandle()

        if self.CreateElement(eeh, self.m_points):
            # Add new line element to active model.
            eeh.AddToModel() 

        if self.CreateDimensionTextElement(t_eeh, self.m_points):
            # Add new line element to active model.
            t_eeh.AddToModel()

        self.m_points.clear()
        # Start of next line is end of current line.
        self.m_points.append(endPt) 

        # Tool should exit after creating a 
        # single line if started in single-shot mode.
        return self._CheckSingleShot() 
       

    def _OnResetButton(self, ev):
        self._OnRestartTool()
        return True
        

    def _OnDynamicFrame(self, ev):
        tmpPts = DPoint3dArray(self.m_points)
        eeh = EditElementHandle()
        t_eeh = EditElementHandle()

        endPt = ev.GetPoint()

        tmpPts.append(endPt)

        if not self.CreateElement(eeh, tmpPts):
            return
        
        if not self.CreateDimensionTextElement(t_eeh, tmpPts):
            return

        redrawElems = RedrawElems()
        # Display in all views, draws to cursor view first...
        redrawElems.SetDynamicsViews(IViewManager.GetActiveViewSet(), 
                                     ev.GetViewport()) 
        redrawElems.SetDrawMode(eDRAW_MODE_TempDraw)
        redrawElems.SetDrawPurpose(DrawPurpose.eDynamics)

        redrawElems.DoRedraw(eeh)
        redrawElems.DoRedraw(t_eeh)


    def CreateElement(self, eeh, points):
        if len(points) != 2:
            return False

        # NOTE: Easier to always work with CurveVector/CurvePrimitive and 
        # not worry about element specific create methods, 
        # ex. LineHandler::CreateLineElement.
        ACTIVEMODEL = ISessionMgr.ActiveDgnModelRef
        if BentleyStatus.eSUCCESS != DraftingElementSchema.ToElement(eeh, 
                                     ICurvePrimitive.CreateLine(DSegment3d(points[0], points[-1])), 
                                     None, ACTIVEMODEL.Is3d(), ACTIVEMODEL):
            return False

        ElementPropertyUtils.ApplyActiveSettings(eeh)

        return True


    def CreateDimensionTextElement(self, t_eeh, points):
        if len(points) != 2:
            return False
        
        ACTIVEMODEL = ISessionMgr.ActiveDgnModelRef
        dgn_file  = ACTIVEMODEL.GetDgnFile()
        dgn_model = ACTIVEMODEL.GetDgnModel()
        model_info = dgn_model.GetModelInfo() 
        mu = model_info.GetUorPerStorage()

        tbProp = TextBlockProperties.Create(dgn_model)
        pProp = ParagraphProperties.Create(dgn_model)
        txStyle = DgnTextStyle.GetSettings(dgn_file)
        rProp = RunProperties.Create(txStyle, dgn_model)
        tb = TextBlock(tbProp, pProp, rProp, dgn_model)

        seg = DSegment3d(points[0], points[-1])
        len_str = str(round(seg.Length()/mu, 2))
        tb.AppendText(len_str)

        midPt = DPoint3d ((points[0].x + points[-1].x)/2, (points[0].y + points[-1].y)/2)
        tb.SetUserOrigin(midPt)

        rmat = RotMatrix ()
        rmat.InitIdentity()
        dAng = math.atan2 (points [-1].y - points [0].y, points [-1].x - points [0].x)
        rmat = RotMatrix.FromVectorAndRotationAngle(DVec3d.From(0,0,1), dAng)
        tb.SetOrientation(rmat)

        status = TextElemHandler.CreateElement(t_eeh, None, tb)
        if TextBlockToElementResult.eTEXTBLOCK_TO_ELEMENT_RESULT_Success != status:
            return False

        ElementPropertyUtils.ApplyActiveSettings(t_eeh)
        
        return True        


    def SetupAndPromptForNextAction(self):
        msgStr = ''
        
        if (len(self.m_points)==0):
            msgStr = 'Enter first point:'
        elif (len(self.m_points)==1):
            msgStr = 'Enter next point:'
        else:
            msgStr = 'Enter next point or reset to complete :'

        NotificationManager.OutputPrompt(msgStr)

        if len(self.m_points) != 2:
            return

        xVec = DVec3d.FromStartEndNormalize(self.m_points[0], self.m_points[-1])

        # Orient AccuDraw to last segment.
        AccuDraw.GetInstance().SetContext(AccuDrawFlags.eACCUDRAW_SetXAxis, None, xVec) 
        

    def InstallNewInstance(toolId, toolPrompt):   
        tool = LineCreator(toolId, toolPrompt) 
        tool.InstallTool()


# Main function
def main():
    LineCreator.InstallNewInstance(0, 0)


# main
if __name__ == "__main__":
    main()

 

Run/Execute project

Load the project “PlaceALine.py” from the Python Manager dialog and Run/Execute the python script.

Click on the screen to create Line elements with dimension texts and is added to your active MicroStation model. Experiment with different events such as Dynamics (mouse move), Reset button (mouse right click) to see how the application functions.

Note: To manage the dimension texts, modify the default Text Style settings such as Text Size, Offset.

 

Video:

 

 

This script provides a straightforward and efficient way to create line elements with dimensions interactively. Feel free to experiment with creating other 2D and 3D elements to meet your needs and seamlessly integrate them into your workflows.

See documentation on Dgn Tools for more details.

 

Happy coding!