Python

Create Centre Line

Create Centre Line 1

This example demonstrates several aspects of MicroStation Python …

Create Centre Line Chords

Dialog Created with PyQt

The UI is created using PyQt standard Widgets. The QComboBox shows a list of cells in the attached cell library. The list is created using helper classes in the cell_library.py module.

The placement interval value is shown in a QLineEdit widget. I've added a QIntValidator to constrain the value that a user may enter.

The cell scale value is shown in a QLineEdit widget. I've added a QDoubleValidator to constrain the value that a user may enter.

There is an unfortunate clash between the messaging mechanism used by MicroStation and that used by PyQt. The work-around is discussed below.

Create Centre Line Start

Create Centre Line Complete

Pick Tool implemented using DgnElementSetTool

cmdPickLineElement inherits from (i.e. is a sub-class of) DgnElementSetTool.

The _OnPostLocate gets an ElementHandle from the supplied HitPath and converts it to a CurveVector …

eh = ElementHandle(path.GetHeadElem(), path.GetRoot())
curve = ICurvePathQuery.ElementToCurveVector(eh)

It tests the CurveVector to verify that is is an open path (curve.IsOpenPath()). It accepts the CurveVector if it contains at least one suitable type …

_ACCEPTABLE_TYPES = (ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_Line,
          ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_LineString,
          ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_Arc,
          ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_CurveVector,
          )

primitiveType = primitive.GetCurvePrimitiveType()
if primitiveType in acceptable_types:
	# We can use this element

Cell Placer demonstrates CurveLocationDetail

Cell placement involves calculating a set of points that lie on the chosen linear element. At each point we extract the tangent to the curve. Class CurveLocationDetail was designed for exactly this purpose.

The logic for this part of the application is mostly copied from the AI-generated code you can read here.

Calculating the tangent is more complex than it need be, because we have to use some other classes (DRay3d and DVec3d) on the way. I've wrapped those steps in function get_tangent_at_point …

def get_tangent_at_point(detail)->RotMatrix:
    ''' Compute the tangent at a point on a line and return its orientation as a rotation matrix. '''
    vr = detail.PointAndUnitTangent()
    ray = vr.Value()
    direction = ray.direction
    x_axis = DVec3d.From (1.0, 0.0, 0.0)
    angle = direction.AngleToXY(x_axis)
    _Z_AXIS = 2
    rotation = RotMatrix.FromAxisAndRotationAngle(_Z_AXIS, angle)
    # At this point we have the tangent orientation
    rotation.Invert()
    # Invert the matrix to rotate the cell to match the tangent
    return rotation

Then we place a cell, using that tangent to apply a rotation. Cell name and cell scale are obtained from the UI.

Work-Around for Messaging Clash

Both PyQt and TkInter use windows messaging. MicroStation uses Windows messaging. Unfortunately, the two messaging mechanisms clash, or more accurately become entangled in a deadlock.

A consequence of such a deadlock is that the host application (MicroStation) can freeze. Worse still, the application may crash (terminate with no explanation).

During development I found that placing any MicroStation function call in a QWidget's action implementation causes a crash. For example, this function crashes MicroStation …

def queue_action_pick_line(self):
    # The following statement crashes MicroStation
    cmdPickLineElement.InstallNewInstance() # Calls the DgnElementSetTool constructor

My work-around removes code that calls into MicroStation from those PyQt action functions. It re-implements the calls outside the PyQt messaging loop by setting an action flag …

def queue_action_pick_line(self):
    self._cmd_pick_line_requested = True

The action flags are tested in the UI's messaging loop …

def ms_mainLoop(self):
  """
  Custom main loop that pumps both Qt and MicroStation events,
  and executes any deferred tool-start calls in a safe context.
  This loop is the key to letting the UI and tool coexist.
  """
  while win32gui.IsWindow(self._storedWinId):
    # Standard pump for a hybrid Qt/MicroStation application
    self._loop.processEvents()
    PyCadInputQueue.PythonMainLoop()
    self._set_requested_flags()

    # Check if a tool start was requested by a QWidget action
    ((mod, cmd), flag) = self._requests.have_request()
    if flag:
      # If yes, run the stored function (e.g., InstallNewInstance)
      print(f"requested '{mod}:{cmd}'")
      invoke_function(mod, cmd, self)
      # Clear the flag so it only runs once per click
      self._requests.clear_requests()

    time.sleep(0.001)

That _requests logic is encapsulated in class CommandRequests (module command_requests.py).

Comments on Work-Around

That logic is clunky and not scaleable. If this application had, say, ten commands and not two then maintenance becomes harder. A permanent solution would be if Bentley Systems could create a better way to raise a barrier between MicroStation messaging and UI messaging.

Download la_solutions_place_cells_along_line.zip

Unpack the ZIP file and copy the Python files into a folder that MicroStation knows about.

Python Manager

Use MicroStation's Python Manager to find and execute the script …


Questions

Post questions about MicroStation Python programming to the MicroStation Programming Forum.