Auto-Dimension Floor Plans

Sorry I havent posted this earlier. But here is the script for Auto-dimensioning floor plans. It isnt perfect, but works for most situations. Feel free to build upon it, add functionality to it, or recommend changes :).

# Author: Ahmed Helmy Abdelmagid
# Description: Creates wall dimensions on all levels or selected level

from pyrevit import DB, forms
from Autodesk.Revit.UI import TaskDialog
from Autodesk.Revit.DB import UV

uiapp = __revit__
app = uiapp.Application
doc = uiapp.ActiveUIDocument.Document

ALL_LEVELS = "All Levels"
SELECT_LEVEL = "Select Level"
OFFSET_MULTIPLIER_INCREMENT = 10
TOLERANCE = doc.Application.ShortCurveTolerance


class UIHandler:
    @staticmethod
    def get_user_input():
        level_options = [ALL_LEVELS, SELECT_LEVEL]
        selected_option = forms.ask_for_one_item(
            level_options, default=ALL_LEVELS, title="Dimension Options"
        )

        selected_level = (
            UIHandler.get_selected_level()
            if selected_option == SELECT_LEVEL
            else None
        )

        selected_face = forms.ask_for_one_item(
            ["Internal", "External"], default="External", title="Dimension Face"
        )
        offset_distance = UIHandler.get_offset_distance()

        if offset_distance is not None:
            offset_distance = DB.UnitUtils.ConvertToInternalUnits(
                offset_distance, DB.UnitTypeId.Millimeters
            )

        return selected_option, selected_level, selected_face, offset_distance

    @staticmethod
    def get_selected_level():
        level_elements = (
            DB.FilteredElementCollector(doc)
            .OfCategory(DB.BuiltInCategory.OST_Levels)
            .WhereElementIsNotElementType()
            .ToElements()
        )
        level_dict = {level.Name: level for level in level_elements}
        selected_level_name = forms.SelectFromList.show(
            sorted(level_dict.keys()), title="Select Level"
        )
        return level_dict.get(selected_level_name)

    @staticmethod
    def get_offset_distance():
        offset_distance_str = forms.ask_for_string(
            default="1000",
            prompt="Enter the offset distance (in millimeters):",
            title="Offset Distance",
        )

        if offset_distance_str:
            try:
                offset_distance = float(offset_distance_str)
                if offset_distance <= 0:
                    TaskDialog.Show(
                        "Error", "Offset distance must be a positive number."
                    )
                    return None
                return offset_distance
            except ValueError:
                TaskDialog.Show(
                    "Error", "Invalid input. Please enter a valid number."
                )
                return None
        else:
            return None

    @staticmethod
    def get_dimensioning_type():
        dimensioning_options = ["Wall Thickness", "Overall"]
        return forms.ask_for_one_item(
            dimensioning_options,
            default="Wall Thickness",
            title="Dimensioning Type",
        )

class GeometryHandler:
    wall_vector_cache = {}

    @staticmethod
    def get_wall_vectors(wall):
        wall_id = wall.Id.IntegerValue
        if wall_id in GeometryHandler.wall_vector_cache:
            return GeometryHandler.wall_vector_cache[wall_id]

        loc_line = wall.Location.Curve
        wall_dir = loc_line.Direction.Normalize()
        perp_dir = wall_dir.CrossProduct(DB.XYZ.BasisZ)

        GeometryHandler.wall_vector_cache[wall_id] = (wall_dir, perp_dir)
        return wall_dir, perp_dir

    @staticmethod
    def get_wall_solid(wall, options=None):
        options = options or DB.Options()

        for geometry_object in wall.get_Geometry(options):
            if isinstance(geometry_object, DB.Solid) and geometry_object.Faces.Size > 0:
                return geometry_object

        return None

    @staticmethod
    def get_wall_outer_edges(wall, opts, dimension_face):
        try:
            wall_solid = GeometryHandler.get_wall_solid(wall, opts)
            if not wall_solid:
                return []

            edges = []
            for face in wall_solid.Faces:
                for edge_loop in face.EdgeLoops:
                    for edge in edge_loop:
                        try:
                            edge_c = edge.AsCurve()
                            if isinstance(edge_c, DB.Line):
                                if edge_c.Direction.IsAlmostEqualTo(
                                    DB.XYZ.BasisZ
                                ) or edge_c.Direction.IsAlmostEqualTo(
                                    -DB.XYZ.BasisZ
                                ):
                                    edges.append(edge)
                        except Exception as e:
                            print(
                                "Error occurred while processing edge for wall {}:".format(
                                    wall.Id
                                )
                            )
                            print("Error: {}".format(str(e)))
                            continue

            if dimension_face == "External":
                edge_endpoints = {}
                outermost_edges = []
                for edge in edges:
                    edge_c = edge.AsCurve()
                    start_point = edge_c.GetEndPoint(0)
                    end_point = edge_c.GetEndPoint(1)
                    if (
                        (start_point, end_point) not in edge_endpoints
                        and (end_point, start_point) not in edge_endpoints
                    ):
                        outermost_edges.append(edge)
                        edge_endpoints[(start_point, end_point)] = True
                return outermost_edges
            else:
                return edges
        except Exception as e:
            print("Error occurred while processing wall {}:".format(wall.Id))
            print("Error: {}".format(str(e)))
            return []

    @staticmethod
    def get_reference_position(edge, wall, dimension_line):
        edge_curve = edge.AsCurve()
        edge_midpoint = edge_curve.Evaluate(0.5, True)
        wall_location = wall.Location.Curve
        intersection_result = dimension_line.Project(edge_midpoint)
        projected_point = intersection_result.XYZPoint
        position = wall_location.Project(projected_point).Parameter
        return position

    @staticmethod
    def get_wall_end_references(wall, options):
        wall_end_references = []
        wall_solid = GeometryHandler.get_wall_solid(wall, options)

        if wall_solid is not None:
            end_faces = GeometryHandler.find_end_faces(wall_solid)
            for face in end_faces:
                wall_end_references.append(face.Reference)

        return wall_end_references

    @staticmethod
    def find_end_faces(solid):
        end_faces = []
        longest_span = None
        max_length = 0

        for edge in solid.Edges:
            edge_curve = edge.AsCurve()
            if edge_curve.Length > max_length:
                longest_span = edge_curve
                max_length = edge_curve.Length

        if longest_span:
            longest_direction = (
                longest_span.GetEndPoint(1) - longest_span.GetEndPoint(0)
            ).Normalize()
            for face in solid.Faces:
                normal = face.ComputeNormal(UV(0.5, 0.5))
                if normal.CrossProduct(longest_direction).IsAlmostEqualTo(
                    DB.XYZ(0, 0, 0)
                ):
                    end_faces.append(face)

        return end_faces

class RevitTransactionManager:
    @staticmethod
    def create_wall_dimensions(
        doc,
        selected_option,
        selected_level,
        selected_face,
        offset_distance,
        tolerance,
        selected_dimensioning_type,
    ):
        level_elements = (
            DB.FilteredElementCollector(doc)
            .OfCategory(DB.BuiltInCategory.OST_Levels)
            .WhereElementIsNotElementType()
            .ToElements()
        )
        existing_dim_endpoints = set()

        for level in level_elements:
            if selected_option == SELECT_LEVEL and level.Id != selected_level.Id:
                continue

            view = RevitAPIUtils.get_floor_plan_view_for_level(doc, level)
            if view is None:
                continue

            walls_on_level = [
                wall
                for wall in RevitGeometryUtils.collect_walls(doc, view.Id, level.Id)
            ]

            if walls_on_level:
                geometry_options = DB.Options()
                geometry_options.ComputeReferences = True
                geometry_options.IncludeNonVisibleObjects = True
                geometry_options.View = view

                for wall in walls_on_level:
                    try:
                        wall_dir, perp_dir = GeometryHandler.get_wall_vectors(wall)
                        existing_line = wall.Location.Curve

                        wall_ext_face_ref = list(
                            DB.HostObjectUtils.GetSideFaces(
                                wall, DB.ShellLayerType.Exterior
                            )
                        )[0]
                        wall_int_face_ref = list(
                            DB.HostObjectUtils.GetSideFaces(
                                wall, DB.ShellLayerType.Interior
                            )
                        )[0]

                        wall_ext_face = wall.GetGeometryObjectFromReference(
                            wall_ext_face_ref
                        )
                        wall_int_face = wall.GetGeometryObjectFromReference(
                            wall_int_face_ref
                        )

                        offset_dir = (
                            -perp_dir
                            if selected_face == "External"
                            else perp_dir
                        )
                        dimensioned_face = (
                            wall_ext_face
                            if selected_face == "External"
                            else wall_int_face
                        )

                        original_off_crv = existing_line.CreateTransformed(
                            DB.Transform.CreateTranslation(
                                offset_dir.Multiply(offset_distance)
                            )
                        )
                        off_crv = original_off_crv

                        vert_edge_sub = DB.ReferenceArray()
                        if selected_dimensioning_type == "Wall Thickness":
                            vert_edges = GeometryHandler.get_wall_outer_edges(
                                wall, geometry_options, dimensioned_face
                            )

                            intersecting_walls = set(
                                RevitAPIUtils.find_intersecting_walls(doc, wall)
                            )
                            vert_edges.extend(
                                edge
                                for int_wall in intersecting_walls
                                for edge in GeometryHandler.get_wall_outer_edges(
                                    int_wall, geometry_options, selected_face
                                )
                            )

                            vert_edges.sort(
                                key=lambda e: e.AsCurve()
                                .GetEndPoint(0)
                                .DistanceTo(existing_line.GetEndPoint(0))
                            )
                            reference_positions = set()

                            for edge in vert_edges:
                                value = round(
                                    DB.UnitUtils.ConvertFromInternalUnits(
                                        edge.ApproximateLength, DB.UnitTypeId.Millimeters
                                    ),
                                    2,
                                )
                                if value > tolerance:
                                    ref_position = GeometryHandler.get_reference_position(
                                        edge, wall, off_crv
                                    )
                                    if not any(
                                        abs(ref_position - existing_position)
                                        < tolerance
                                        for existing_position in reference_positions
                                    ):
                                        vert_edge_sub.Append(edge.Reference)
                                        reference_positions.add(ref_position)

                        else:  # "Overall" method
                            wall_end_references = (
                                GeometryHandler.get_wall_end_references(
                                    wall, geometry_options
                                )
                            )
                            for ref in wall_end_references:
                                vert_edge_sub.Append(ref)

                        if vert_edge_sub.Size >= 2:
                            line = off_crv
                            offset_multiplier = 1
                            while not RevitAPIUtils.is_space_free_for_dimension(
                                doc, line, view
                            ):
                                offset_multiplier += 1
                                line = RevitAPIUtils.offset_dimension_line(
                                    original_off_crv,
                                    OFFSET_MULTIPLIER_INCREMENT * offset_multiplier,
                                )

                            dim_line = DB.Line.CreateBound(
                                line.GetEndPoint(0), line.GetEndPoint(1)
                            )
                            dim_tuple = tuple(
                                sorted((dim_line.GetEndPoint(0), dim_line.GetEndPoint(1)))
                            )
                            if dim_tuple not in existing_dim_endpoints:
                                dim = doc.Create.NewDimension(view, dim_line, vert_edge_sub)
                                existing_dim_endpoints.add(dim_tuple)

                    except Exception as e:
                        print(
                            "Failed to create dimension for wall with ID {}: {}".format(
                                wall.Id, e
                            )
                        )
                        continue

class RevitGeometryUtils:
    @staticmethod
    def collect_walls(doc, view_id, level_id):
        wall_collector = (
            DB.FilteredElementCollector(doc, view_id)
            .OfCategory(DB.BuiltInCategory.OST_Walls)
            .WherePasses(DB.ElementLevelFilter(level_id))
            .WhereElementIsNotElementType()
            .ToElements()
        )
        return wall_collector

class RevitAPIUtils:
    @staticmethod
    def get_floor_plan_view_for_level(doc, level):
        view_collector = (
            DB.FilteredElementCollector(doc).OfClass(DB.ViewPlan).ToElements()
        )
        return next(
            (
                view
                for view in view_collector
                if view.GenLevel is not None
                and view.GenLevel.Name == level.Name
                and not view.IsTemplate
            ),
            None,
        )

    @staticmethod
    def find_intersecting_walls(doc, wall):
        bbox = wall.get_BoundingBox(None)
        outline = DB.Outline(bbox.Min, bbox.Max)
        intersecting_walls = {
            w
            for w in DB.FilteredElementCollector(doc)
            .OfClass(DB.Wall)
            .WherePasses(DB.BoundingBoxIntersectsFilter(outline))
            .WhereElementIsNotElementType()
            .ToElements()
            if w.Id != wall.Id
        }
        return intersecting_walls

    @staticmethod
    def is_space_free_for_dimension(doc, line, view):
        start_pt = line.GetEndPoint(0)
        end_pt = line.GetEndPoint(1)

        min_point = DB.XYZ(min(start_pt.X, end_pt.X), start_pt.Y - 0.5, 0)
        max_point = DB.XYZ(max(start_pt.X, end_pt.X), start_pt.Y + 0.5, 0)

        outline = DB.Outline(min_point, max_point)

        intersecting_dimensions = (
            DB.FilteredElementCollector(doc, view.Id)
            .OfCategory(DB.BuiltInCategory.OST_Dimensions)
            .WherePasses(DB.BoundingBoxIntersectsFilter(outline))
            .WhereElementIsNotElementType()
            .ToElements()
        )

        return len(intersecting_dimensions) == 0

    @staticmethod
    def offset_dimension_line(line, offset_value):
        transform = DB.Transform.CreateTranslation(DB.XYZ(0, offset_value, 0))
        return line.CreateTransformed(transform)

def main():
    try:
        selected_option, selected_level, selected_face, offset_distance = (
            UIHandler.get_user_input()
        )
        selected_dimensioning_type = UIHandler.get_dimensioning_type()

        if offset_distance is None:
            TaskDialog.Show("Warning", "Operation cancelled by the user.")
        else:
            with DB.Transaction(doc, "Auto Dimension Walls") as transaction:
                try:
                    transaction.Start()
                    RevitTransactionManager.create_wall_dimensions(
                        doc,
                        selected_option,
                        selected_level,
                        selected_face,
                        offset_distance,
                        TOLERANCE,
                        selected_dimensioning_type,
                    )
                    transaction.Commit()
                except Exception as e:
                    transaction.RollBack()
                    TaskDialog.Show(
                        "Error",
                        "An error occurred while dimensioning walls:\n{}".format(str(e)),
                    )
                    raise
    except Exception as e:
        TaskDialog.Show("Error", "An unexpected error occurred:\n{}".format(str(e)))
        raise

if __name__ == "__main__":
    main()
6 Likes

It would be amazing getting some feedback if possible @Jean-Marc

Hi @Helmy000 , thanks for the contribution!

I took the liberty to move the discussion to the “showcase” category.

I still have to run and analyze that code, but here’s my first observation: may I ask you why you’re using classes with only static methods in them?
This is a c# thing since (almost) everything needs to be a class, but in python you just write functions!

If you need to organize the functions, just put them in different modules (files) and import them in the main script.py.

And you can go even further by moving reusable functions and modules in a lib folder of your extension, so they are accessible by all the scripts.

Hi Andrea,

Absolutely agree, it’s not a python methodology at all. Its a learned habit that I haven’t unlearned.

1 Like

@Helmy000
It’s really awesome script. Don’t know why I can’t make it working in some of our rvt files because it reports a list of errors and no dimensions. I don’t have time right now to investigates why it reports those errors.

It doesn’t work with curved walls because it sees them as arcs, but they don’t have any direction.

Also it would be really amazing if it could be possible to select few walls in order to dimensions only them

Really well done with your job, continue improving!

Hi Fabio,

Thank you for your words. The script at the moment does not consider arcs at all, this is a current limitation and I will be working to add that in the future. Regarding selecting a few walls, that is an easy adjustment that can be included.

1 Like