Drawing an Analytical Member

Hello everyone, I’m new here and just starting out with pyRevit.

For practice, I’d like to recreate the “Member” button under Analyze to place an analytical member between two points. I imagine I need to use the PickPoint class to select the two points and then pass them to the class that represents analytical members (which I haven’t been able to find).

Do you think this is too complicated as a starter example?

My attempt is

with Transaction(doc,'test') as t:
    t.Start()
    p1= uidoc.Selection.PickPoint()
    p2= uidoc.Selection.PickPoint()
    line = Line.CreateBound(p1,p2)
    analine = AnalyticalMember.Create(doc,line)
    t.Commit()

it seems working fine but I am just picking two points and then the element is visualised. I wonder if there is a way to create a preview of the line

Hi, welcome

What are the exact steps you would like to go through or have?

  1. Pick point 1
  2. Pick point 2
  3. Make a 3d model line
    4a. Validate
  4. Create the member

4b. Cancel and exist the script

?

Jean-Marc, thank you

In the code above, the user selects two points, and an analytical member is then drawn. What I would like to implement is a kind of preview, similar to what we see when drawing a line in Revit. After selecting the first point, a line should be visualized dynamically according to the position of the pointer.

I am wondering whether, instead of requiring two picked points, I could use a Line class and then convert it into an analytical member. I would like to know if I am on the right track or if I am overcomplicating the approach.

Thank you.

I don’t think what you’re looking for can be done directly via revit API.

You could draw a preview of what you’re creating via the direct context 3d interface, but that however is an overcomplicated approach to this imo.

Anyway, I tried to implement sth like you’re descriping with the help of AI, quite fun, but not sth worth rolling out into a productive environment.
Here’s the code in case anyone has a similiar usecase, the custom tooltip by thebuildingcoder can be used a baseline to start with as well.
Recording 2025-12-06 175529

from pyrevit import framework, revit, HOST_APP, script, forms
from pyrevit import DB, UI

__persistentengine__ = True

# Global state
dc3d_server = None
tracker = None
idling_handler = None
selection_handler = None
p1 = None
p2 = None
ui_instance = None


def create_line_mesh(start, end, color=DB.ColorWithTransparency(255, 0, 0, 0)):
    edge = revit.dc3dserver.Edge(start, end, color)
    mesh = revit.dc3dserver.Mesh([edge], [])
    return mesh


def create_cube_mesh(center, size=0.3, color=DB.ColorWithTransparency(255, 0, 0, 0)):
    bb = DB.BoundingBoxXYZ()
    bb.Min = DB.XYZ(-size, -size, -size)
    bb.Max = DB.XYZ(size, size, size)
    bb.Transform = DB.Transform.CreateTranslation(center)
    mesh = revit.dc3dserver.Mesh.from_boundingbox(bb, color, black_edges=True)
    return mesh


def get_mouse_model_point():
    ui_view = revit.active_ui_view
    if not ui_view:
        return None

    # --- 1. Get the current Windows mouse position ---
    p = framework.Forms.Cursor.Position

    # --- 2. Get the Revit UIView window rectangle ---
    rect = ui_view.GetWindowRectangle()

    # Check bounds
    if p.X < rect.Left or p.X > rect.Right:
        return None
    if p.Y < rect.Top or p.Y > rect.Bottom:
        return None

    # --- 3. Compute relative mouse position inside the view ---
    dx = float(p.X - rect.Left) / float(rect.Right - rect.Left)
    dy = float(p.Y - rect.Bottom) / float(rect.Top - rect.Bottom)

    # NOTE: dy uses Bottom->Top order because Windows Y grows downwards

    # --- 4. Convert to model coordinates using zoom corners ---
    corners = ui_view.GetZoomCorners()
    a = corners[0]   # bottom-left point in model coords
    b = corners[1]   # top-right point in model coords

    v = b - a  # diagonal of the view box

    # Build the point in model coordinates
    q = DB.XYZ(
        a.X + dx * v.X,
        a.Y + dy * v.Y,
        a.Z + dx * v.Z  # Perspective views have z variation
    )

    return q


class MouseTracker:
    def __init__(self, start_point, dc3d_server_instance):
        self.start_point = start_point
        self.dc3d_server = dc3d_server_instance
        self.is_active = False
        self.last_mouse_point = None
        self.frame_skip = 0

    def on_idling(self, sender, args):
        """Called during Revit's idle time"""
        if not self.is_active:
            return

        self.frame_skip += 1
        if self.frame_skip < 2:
            return
        self.frame_skip = 0

        try:
            mouse_point = get_mouse_model_point()

            if mouse_point is None:
                return

            if self.last_mouse_point is not None:
                delta = mouse_point.DistanceTo(self.last_mouse_point)
                if delta < 0.1:
                    return

            self.last_mouse_point = mouse_point

            line_color = DB.ColorWithTransparency(0, 255, 0, 100)
            preview_line = create_line_mesh(self.start_point, mouse_point, line_color)

            # Show start point cube and preview line
            start_cube = create_cube_mesh(
                self.start_point, 0.2, DB.ColorWithTransparency(255, 0, 0, 0)
            )
            self.dc3d_server.meshes = [start_cube, preview_line]
            revit.uidoc.RefreshActiveView()

        except Exception as e:
            print("Error in mouse tracking: " + str(e))

    def start(self):
        self.is_active = True

    def stop(self):
        self.is_active = False


def on_selection_changed(sender, args):
    """Triggered when user clicks in Revit view after selecting p1."""
    global tracker, selection_handler

    # Trigger second pick
    ui_instance.end_event.Raise()


# External Event Handlers
class PickStartPointHandler(UI.IExternalEventHandler):
    def Execute(self, uiapp):
        global p1, tracker, idling_handler, dc3d_server, ui_instance, selection_handler
        try:
            p1 = revit.pick_point("Pick start point")
            if p1:
                # Show start point cube
                start_cube = create_cube_mesh(
                    p1, 0.2, DB.ColorWithTransparency(255, 0, 0, 0)
                )
                dc3d_server.meshes = [start_cube]
                revit.uidoc.RefreshActiveView()

                # Set up mouse tracking
                tracker = MouseTracker(p1, dc3d_server)
                idling_handler = framework.EventHandler[UI.Events.IdlingEventArgs](
                    tracker.on_idling
                )
                HOST_APP.uiapp.Idling += idling_handler
                tracker.start()

                selection_handler = framework.EventHandler[UI.Events.SelectionChangedEventArgs](on_selection_changed)
                HOST_APP.uiapp.SelectionChanged += selection_handler

                # Update UI state
                if ui_instance:
                    ui_instance.Dispatcher.Invoke(
                        framework.System.Action(ui_instance.update_after_start_point)
                    )
        except Exception as ex:
            print("Error picking start point: " + str(ex))

    def GetName(self):
        return "Pick Start Point Handler"


class PickEndPointHandler(UI.IExternalEventHandler):
    def Execute(self, uiapp):
        global p1, p2, tracker, dc3d_server, ui_instance, selection_handler
        try:
            if selection_handler:
                try:
                    HOST_APP.uiapp.SelectionChanged -= selection_handler
                except:
                    pass
                selection_handler = None
            p2 = revit.pick_point("Pick end point")
            if p2:
                # Stop tracking
                if tracker:
                    tracker.stop()

                # Show both points and final line
                start_cube = create_cube_mesh(
                    p1, 0.2, DB.ColorWithTransparency(255, 0, 0, 0)
                )
                end_cube = create_cube_mesh(
                    p2, 0.2, DB.ColorWithTransparency(0, 0, 255, 0)
                )
                final_line = create_line_mesh(
                    p1, p2, DB.ColorWithTransparency(0, 255, 0, 0)
                )
                dc3d_server.meshes = [start_cube, end_cube, final_line]
                revit.uidoc.RefreshActiveView()

                # Update UI state
                if ui_instance:
                    distance = p1.DistanceTo(p2)
                    ui_instance.Dispatcher.Invoke(
                        framework.System.Action(
                            lambda: ui_instance.update_after_end_point(distance)
                        )
                    )
        except Exception as ex:
            print("Error picking end point: " + str(ex))

    def GetName(self):
        return "Pick End Point Handler"


class CreateLineHandler(UI.IExternalEventHandler):
    def Execute(self, uiapp):
        global p1, p2, dc3d_server, ui_instance
        try:
            if p1 and p2:
                with revit.Transaction("Create Model Line"):
                    line = DB.Line.CreateBound(p1, p2)

                    sketch_plane = revit.doc.ActiveView.SketchPlane
                    if sketch_plane is None:
                        plane = DB.Plane.CreateByNormalAndOrigin(DB.XYZ.BasisZ, p1)
                        sketch_plane = DB.SketchPlane.Create(revit.doc, plane)

                    model_line = revit.doc.Create.NewModelCurve(line, sketch_plane)

                print("Model line created successfully!")

                # Clear preview meshes
                dc3d_server.meshes = []
                revit.uidoc.RefreshActiveView()

                # Update UI state
                if ui_instance:
                    ui_instance.Dispatcher.Invoke(
                        framework.System.Action(ui_instance.reset_ui)
                    )
        except Exception as ex:
            print("Error creating model line: " + str(ex))

    def GetName(self):
        return "Create Line Handler"


class PreviewLineUI(forms.WPFWindow):
    def __init__(self):
        global ui_instance, dc3d_server

        # Create inline XAML
        xaml_string = """
        <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                Title="Preview Line Tool" 
                Width="300" Height="340"
                WindowStartupLocation="CenterScreen"
                ShowInTaskbar="False"
                Topmost="True">
            <StackPanel Margin="20">
                <TextBlock x:Name="txtStatus" 
                          Text="Click 'Start' to begin" 
                          FontSize="14" 
                          Margin="0,0,0,20"
                          TextWrapping="Wrap"/>
                
                <TextBlock x:Name="txtInfo" 
                          Text="" 
                          FontSize="11" 
                          Foreground="Gray"
                          Margin="0,0,0,20"
                          TextWrapping="Wrap"
                          Height="50"/>
                
                <Button x:Name="btnStart" 
                       Content="Pick Start Point" 
                       Height="35" 
                       Margin="0,0,0,10"
                       FontSize="14"/>
                
                <Button x:Name="btnEnd" 
                       Content="Pick End Point" 
                       Height="35" 
                       Margin="0,0,0,10"
                       FontSize="14"
                       IsEnabled="False"/>
                
                <Button x:Name="btnCreate" 
                       Content="Create Model Line" 
                       Height="35"
                       FontSize="14"
                       IsEnabled="False"/>
            </StackPanel>
        </Window>
        """

        forms.WPFWindow.__init__(self, xaml_source=xaml_string, literal_string=True)

        dc3d_server = revit.dc3dserver.Server(uidoc=revit.uidoc)
        if not dc3d_server:
            script.exit()

        ui_instance = self

        # Create external events
        self.start_handler = PickStartPointHandler()
        self.start_event = UI.ExternalEvent.Create(self.start_handler)

        self.end_handler = PickEndPointHandler()
        self.end_event = UI.ExternalEvent.Create(self.end_handler)

        self.create_handler = CreateLineHandler()
        self.create_event = UI.ExternalEvent.Create(self.create_handler)

        # Wire up button events
        self.btnStart.Click += self.on_start_click
        self.btnEnd.Click += self.on_end_click
        self.btnCreate.Click += self.on_create_click

        self.Closed += self.form_closed

    def on_start_click(self, sender, args):
        self.txtStatus.Text = "Pick the start point in the view..."
        self.start_event.Raise()

    def on_end_click(self, sender, args):
        self.txtStatus.Text = "Pick the end point in the view..."
        self.end_event.Raise()

    def on_create_click(self, sender, args):
        self.txtStatus.Text = "Creating model line..."
        self.create_event.Raise()

    def update_after_start_point(self):
        global p1
        self.btnStart.IsEnabled = False
        self.btnEnd.IsEnabled = True
        self.btnCreate.IsEnabled = False
        self.txtStatus.Text = "Start point selected. Move mouse to preview."
        coord_text = "Start: ({0:.2f}, {1:.2f}, {2:.2f})".format(p1.X, p1.Y, p1.Z)
        self.txtInfo.Text = coord_text

    def update_after_end_point(self, distance):
        global p1, p2
        self.btnStart.IsEnabled = False
        self.btnEnd.IsEnabled = False
        self.btnCreate.IsEnabled = True
        self.txtStatus.Text = "Both points selected. Ready to create line."
        coord_text = "Start: ({0:.2f}, {1:.2f}, {2:.2f})\nEnd: ({3:.2f}, {4:.2f}, {5:.2f})\nLength: {6:.2f}".format(
            p1.X, p1.Y, p1.Z, p2.X, p2.Y, p2.Z, distance
        )
        self.txtInfo.Text = coord_text

    def reset_ui(self):
        global p1, p2
        p1 = None
        p2 = None
        self.btnStart.IsEnabled = True
        self.btnEnd.IsEnabled = False
        self.btnCreate.IsEnabled = False
        self.txtStatus.Text = "Line created! Click 'Start' for another."
        self.txtInfo.Text = ""

    def form_closed(self, sender, args):
        """Clean up when form is closed"""
        global tracker, idling_handler, dc3d_server, selection_handler

        if tracker:
            tracker.stop()
        if idling_handler:
            try:
                HOST_APP.uiapp.Idling -= idling_handler
            except:
                pass
        if selection_handler:
            try:
                HOST_APP.uiapp.SelectionChanged -= selection_handler
            except:
                pass
        # Clear preview meshes
        if dc3d_server:
            dc3d_server.meshes = []
            revit.uidoc.RefreshActiveView()


# Launch the modeless window
if __name__ == "__main__":
    PreviewLineUI().Show()
1 Like

Thanks pyrevit,

In effect, it looks very complicated. I have found more info on the Revit forum, and I understood that what I am looking for is called “rubber band” which is not available in Revit API.

I am now looking to create and visualise, at least, the first point, so the user can understand where it is. I will try to read your code and see how those circles appear in your case. I am not able to do the same with my code