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()
8 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

Just saw someone liking this recently, here is a python version (in terms of structure :slight_smile:

# Author: Ahmed Helmy Abdelmagid
# Description: Creates wall dimensions on all levels or selected level with arc handling
import clr
import json
import os
import time
import traceback
import math

# Revit API References
clr.AddReference('RevitAPI')
clr.AddReference('RevitAPIUI')
from Autodesk.Revit import DB
from Autodesk.Revit.UI import TaskDialog
from Autodesk.Revit.Exceptions import OperationCanceledException

# pyRevit Imports
from pyrevit import forms, script

# System Collections
from System.Collections.Generic import List

# Initialize Logger
logger = script.get_logger()

# Constants
ALL_LEVELS = "All Levels"
SELECT_LEVEL = "Select Level"
OFFSET_MULTIPLIER_INCREMENT = 10
DEFAULT_OFFSET_MM = 1000  # Default offset in millimetres
TOLERANCE = 0.01  # Constant tolerance value

# Initialize Revit Document
uiapp = __revit__
app = uiapp.Application
doc = uiapp.ActiveUIDocument.Document

# ============================================
# Configuration Management
# ============================================
class Config:
    """
    Handles loading, saving, and managing user configuration.
    """
    def __init__(self):
        self.config_path = os.path.join(os.path.dirname(__file__), 'config.json')
        self.data = self.load()

    def load(self):
        if os.path.exists(self.config_path):
            try:
                with open(self.config_path, 'r') as f:
                    return json.load(f)
            except Exception as e:
                logger.error("Failed to load config: {}".format(str(e)))
                return {}
        return {}

    def save(self):
        try:
            with open(self.config_path, 'w') as f:
                json.dump(self.data, f, indent=2)
        except Exception as e:
            logger.error("Failed to save config: {}".format(str(e)))

    def get(self, key, default=None):
        return self.data.get(key, default)

    def set(self, key, value):
        self.data[key] = value

    def remove(self, key):
        self.data.pop(key, None)

# ============================================
# Unit Conversion Utility
# ============================================
def mm_to_internal(mm):
    return DB.UnitUtils.ConvertToInternalUnits(mm, DB.UnitTypeId.Millimeters)

def internal_to_mm(internal):
    return DB.UnitUtils.ConvertFromInternalUnits(internal, DB.UnitTypeId.Millimeters)

# ============================================
# User Interface Management
# ============================================
def get_selected_level():
    """
    Prompts the user to select a specific 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)

def get_offset_distance():
    """
    Prompts the user to input the offset distance in millimetres.
    """
    offset_distance_str = forms.ask_for_string(
        default="1000",
        prompt="Enter the offset distance (in millimetres):",
        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

def get_dimensioning_type():
    """
    Prompts the user to select the dimensioning type.
    """
    dimensioning_options = ["Wall Thickness", "Overall"]
    return forms.ask_for_one_item(
        dimensioning_options,
        default="Wall Thickness",
        title="Dimensioning Type",
    )

def get_user_input():
    """
    Collects user inputs for dimensioning options, including Dimensioning Type.
    """
    level_options = [ALL_LEVELS, SELECT_LEVEL]
    selected_option = forms.ask_for_one_item(
        level_options, default=ALL_LEVELS, title="Dimension Options"
    )
    if not selected_option:
        logger.warning("No dimension option selected. Operation cancelled.")
        return None, None, None, None, None

    if selected_option == SELECT_LEVEL:
        selected_level = get_selected_level()
        if not selected_level:
            logger.warning("No level selected. Operation cancelled.")
            forms.alert("No level selected. Operation cancelled.", exitscript=True)
            return None, None, None, None, None
    else:
        selected_level = None

    selected_face = forms.ask_for_one_item(
        ["Internal", "External"], default="External", title="Dimension Face"
    )
    if not selected_face:
        logger.warning("No dimension face selected. Operation cancelled.")
        forms.alert("No dimension face selected. Operation cancelled.", exitscript=True)
        return None, None, None, None, None

    offset_distance = get_offset_distance()
    if offset_distance is None:
        logger.warning("Invalid offset distance. Operation cancelled.")
        forms.alert("Invalid offset distance. Operation cancelled.", exitscript=True)
        return None, None, None, None, None

    selected_dimensioning_type = get_dimensioning_type()
    if not selected_dimensioning_type:
        logger.warning("No dimensioning type selected. Operation cancelled.")
        forms.alert("No dimensioning type selected. Operation cancelled.", exitscript=True)
        return None, None, None, None, None

    # Convert offset distance to internal units
    offset_distance_internal = mm_to_internal(offset_distance)
    return selected_option, selected_level, selected_face, offset_distance_internal, selected_dimensioning_type

# ============================================
# Geometry Handling
# ============================================
_wall_vector_cache = {}

def get_wall_vectors(wall):
    wall_id = wall.Id.IntegerValue
    if wall_id in _wall_vector_cache:
        return _wall_vector_cache[wall_id]

    loc_curve = wall.Location.Curve
    if isinstance(loc_curve, DB.Line):
        wall_dir = loc_curve.Direction.Normalize()
    elif isinstance(loc_curve, DB.Arc):
        param = 0.5  # Mid-point parameter
        derivatives = loc_curve.ComputeDerivatives(param, True)
        wall_dir = derivatives.BasisX.Normalize()
    else:
        logger.warning("Unsupported curve type for wall location: {}. Defaulting to X direction.".format(type(loc_curve)))
        wall_dir = DB.XYZ.BasisX

    perp_dir = wall_dir.CrossProduct(DB.XYZ.BasisZ)
    _wall_vector_cache[wall_id] = (wall_dir, perp_dir)
    return wall_dir, perp_dir

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

def get_wall_outer_edges(wall, opts, dimension_face):
    try:
        wall_solid = get_wall_solid(wall, opts)
        if not wall_solid:
            logger.warning("No solid geometry found for wall ID {}".format(wall.Id))
            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)
                        elif isinstance(edge_c, DB.Arc):
                            # For arcs, append the entire arc
                            edges.append(edge)
                    except Exception as e:
                        logger.error("Error occurred while processing edge for wall {}: {}".format(wall.Id, str(e)))
                        continue

        if dimension_face == "External":
            edge_endpoints = {}
            outermost_edges = []
            for edge in edges:
                edge_c = edge.AsCurve()
                if isinstance(edge_c, DB.Line):
                    start_point = edge_c.GetEndPoint(0)
                    end_point = edge_c.GetEndPoint(1)
                elif isinstance(edge_c, DB.Arc):
                    start_point = edge_c.GetEndPoint(0)
                    end_point = edge_c.GetEndPoint(1)
                else:
                    continue  # Unsupported curve type

                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:
        logger.error("Error occurred while processing wall {}: {}".format(wall.Id, str(e)))
        return []

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

def get_wall_end_references(wall, options):
    wall_end_references = []
    wall_solid = get_wall_solid(wall, options)
    if wall_solid is not None:
        end_faces = find_end_faces(wall_solid)
        for face in end_faces:
            wall_end_references.append(face.Reference)
    return wall_end_references

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(DB.UV(0.5, 0.5))
            if normal.CrossProduct(longest_direction).IsAlmostEqualTo(DB.XYZ.Zero):
                end_faces.append(face)
    return end_faces

# ============================================
# Revit API Utilities
# ============================================
def get_floor_plan_view_for_level(doc, level):
    view_collector = DB.FilteredElementCollector(doc).OfClass(DB.ViewPlan).ToElements()
    for view in view_collector:
        if view.GenLevel is not None and view.GenLevel.Name == level.Name and not view.IsTemplate:
            return view
    return None

def find_intersecting_walls(doc, wall):
    bbox = wall.get_BoundingBox(None)
    if not bbox:
        logger.warning("No bounding box found for wall ID {}".format(wall.Id))
        return set()

    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

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), min(start_pt.Y, end_pt.Y), min(start_pt.Z, end_pt.Z))
    max_point = DB.XYZ(max(start_pt.X, end_pt.X), max(start_pt.Y, end_pt.Y), max(start_pt.Z, end_pt.Z))
    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

def offset_dimension_line(line, offset_value, perp_dir):
    """
    Offsets the dimension line in the direction perpendicular to the wall.
    """
    return line.CreateTransformed(DB.Transform.CreateTranslation(perp_dir.Multiply(offset_value)))

def find_free_space_for_dimension(doc, original_line, perp_dir, view):
    line = original_line
    offset_multiplier = 1
    max_attempts = 10  # Prevent infinite loops
    while not is_space_free_for_dimension(doc, line, view) and offset_multiplier < max_attempts:
        offset_multiplier += 1
        line = offset_dimension_line(original_line, OFFSET_MULTIPLIER_INCREMENT * offset_multiplier, perp_dir)
        logger.debug("Offsetting dimension line by {}mm to find free space.".format(OFFSET_MULTIPLIER_INCREMENT * offset_multiplier))
    if offset_multiplier >= max_attempts:
        logger.warning("Max attempts reached for finding free space for dimension.")
        return None
    return line

def create_arc_length_dimension(doc, view, arc, arc_ref, first_ref, second_ref):
    """
    Creates an arc length dimension for arc walls.
    """
    try:
        ref_array = DB.ReferenceArray()
        ref_array.Append(first_ref)
        ref_array.Append(second_ref)
        # Use the enhanced NewArcLengthDimension method from Revit 2024
        dimension = doc.Create.NewArcLengthDimension(view, arc, arc_ref, ref_array)
        logger.info("Successfully created arc length dimension for arc ID {}.".format(arc.Id))
        return dimension
    except Exception as e:
        logger.error("Failed to create arc length dimension for arc ID {}: {}".format(arc.Id, str(e)))
        logger.error(traceback.format_exc())
        return None

# ============================================
# Revit Geometry Utilities
# ============================================
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()
    )
    logger.debug("Collected {} walls from view ID {} on level ID {}.".format(len(wall_collector), view_id, level_id))
    return wall_collector

# ============================================
# Revit Transaction Management
# ============================================
def create_wall_dimensions(doc, selected_option, selected_level, selected_face, offset_distance, 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 = get_floor_plan_view_for_level(doc, level)
        if view is None:
            logger.warning("No floor plan view found for level '{}'.".format(level.Name))
            continue

        walls_on_level = collect_walls(doc, view.Id, level.Id)
        if not walls_on_level:
            logger.info("No walls found on level '{}'.".format(level.Name))
            continue

        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 = get_wall_vectors(wall)
                existing_line = wall.Location.Curve

                wall_ext_face_refs = list(DB.HostObjectUtils.GetSideFaces(wall, DB.ShellLayerType.Exterior))
                wall_int_face_refs = list(DB.HostObjectUtils.GetSideFaces(wall, DB.ShellLayerType.Interior))
                if not wall_ext_face_refs or not wall_int_face_refs:
                    logger.warning("Wall ID {} does not have both exterior and interior faces.".format(wall.Id))
                    continue

                wall_ext_face_ref = wall_ext_face_refs[0]
                wall_int_face_ref = wall_int_face_refs[0]

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

                offset_dir = perp_dir.Negate() 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()
                is_arc_wall = False
                arc = None
                arc_ref = None

                if selected_dimensioning_type == "Wall Thickness":
                    vert_edges = get_wall_outer_edges(wall, geometry_options, selected_face)
                    intersecting_walls = find_intersecting_walls(doc, wall)
                    logger.debug("Wall ID {} has {} intersecting walls.".format(wall.Id, len(intersecting_walls)))
                    if intersecting_walls:
                        for int_wall in intersecting_walls:
                            int_vert_edges = get_wall_outer_edges(int_wall, geometry_options, selected_face)
                            vert_edges.extend(int_vert_edges)
                    else:
                        logger.info("Wall ID {} has no intersecting walls.".format(wall.Id))

                    if not vert_edges:
                        logger.warning("No vertical edges found for wall ID {}.".format(wall.Id))
                        continue

                    vert_edges.sort(key=lambda e: e.AsCurve().GetEndPoint(0).DistanceTo(existing_line.GetEndPoint(0)))
                    reference_positions = set()
                    for edge in vert_edges:
                        curve = edge.AsCurve()
                        if isinstance(curve, DB.Line):
                            value = round(DB.UnitUtils.ConvertFromInternalUnits(curve.Length, DB.UnitTypeId.Millimeters), 2)
                        elif isinstance(curve, DB.Arc):
                            arc = curve
                            radius = arc.Radius
                            length = arc.Length
                            theta = length / radius  # Central angle in radians
                            chord_length = 2 * radius * math.sin(theta / 2)
                            value = round(DB.UnitUtils.ConvertFromInternalUnits(chord_length, DB.UnitTypeId.Millimeters), 2)
                            is_arc_wall = True
                            arc_ref = edge.Reference
                        else:
                            value = 0  # Unsupported curve type
                        if value > TOLERANCE:
                            ref_position = 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)
                                logger.debug("Appended reference from wall ID {}.".format(wall.Id))

                    if is_arc_wall and selected_dimensioning_type == "Overall":
                        if isinstance(existing_line, DB.Arc):
                            arc = existing_line
                            arc_ref = wall_ext_face_ref  # Assuming exterior face
                            if vert_edge_sub.Size >= 2:
                                first_ref = vert_edge_sub.get_Item(0)
                                second_ref = vert_edge_sub.get_Item(1)
                                create_arc_length_dimension(doc, view, arc, arc_ref, first_ref, second_ref)
                            else:
                                logger.warning("Not enough references to create angular dimension for arc wall ID {}.".format(wall.Id))
                        else:
                            logger.warning("Expected arc wall but found non-arc wall ID {}.".format(wall.Id))
                            continue
                    else:
                        line = find_free_space_for_dimension(doc, off_crv, perp_dir, view)
                        if line is None:
                            logger.warning("No free space found for dimension line for wall ID {}".format(wall.Id))
                            continue

                        dim_line = DB.Line.CreateBound(line.GetEndPoint(0), line.GetEndPoint(1))
                        if is_space_free_for_dimension(doc, dim_line, view):
                            dim_tuple = tuple(sorted([
                                (dim_line.GetEndPoint(0).X, dim_line.GetEndPoint(0).Y, dim_line.GetEndPoint(0).Z),
                                (dim_line.GetEndPoint(1).X, dim_line.GetEndPoint(1).Y, dim_line.GetEndPoint(1).Z)
                            ]))
                            if dim_tuple not in existing_dim_endpoints:
                                try:
                                    dim = doc.Create.NewDimension(view, dim_line, vert_edge_sub)
                                    existing_dim_endpoints.add(dim_tuple)
                                    logger.info("Successfully created dimension for wall ID {}.".format(wall.Id))
                                except OperationCanceledException:
                                    logger.warning("Dimension creation cancelled by user.")
                                    continue
                                except Exception as e:
                                    logger.error("Failed to create dimension for wall ID {}: {}".format(wall.Id, str(e)))
                                    logger.error(traceback.format_exc())
                                    continue
                            else:
                                logger.info("Duplicate dimension detected for wall ID {}. Skipping.".format(wall.Id))
                        else:
                            logger.info("Dimension line already exists for wall ID {}. Skipping.".format(wall.Id))
                else:  # "Overall" method
                    wall_end_references = get_wall_end_references(wall, geometry_options)
                    for ref in wall_end_references:
                        vert_edge_sub.Append(ref)
                        logger.debug("Appended end reference from wall ID {}.".format(wall.Id))
                    if vert_edge_sub.Size >= 2:
                        if is_arc_wall and isinstance(existing_line, DB.Arc):
                            arc = existing_line
                            arc_ref = wall_ext_face_ref  # Assuming exterior face
                            if vert_edge_sub.Size >= 2:
                                first_ref = vert_edge_sub.get_Item(0)
                                second_ref = vert_edge_sub.get_Item(1)
                                create_arc_length_dimension(doc, view, arc, arc_ref, first_ref, second_ref)
                            else:
                                logger.warning("Not enough references to create angular dimension for arc wall ID {}.".format(wall.Id))
                        else:
                            line = find_free_space_for_dimension(doc, off_crv, perp_dir, view)
                            if line is None:
                                logger.warning("No free space found for dimension line for wall ID {}".format(wall.Id))
                                continue

                            dim_line = DB.Line.CreateBound(line.GetEndPoint(0), line.GetEndPoint(1))
                            if is_space_free_for_dimension(doc, dim_line, view):
                                dim_tuple = tuple(sorted([
                                    (dim_line.GetEndPoint(0).X, dim_line.GetEndPoint(0).Y, dim_line.GetEndPoint(0).Z),
                                    (dim_line.GetEndPoint(1).X, dim_line.GetEndPoint(1).Y, dim_line.GetEndPoint(1).Z)
                                ]))
                                if dim_tuple not in existing_dim_endpoints:
                                    try:
                                        dim = doc.Create.NewDimension(view, dim_line, vert_edge_sub)
                                        existing_dim_endpoints.add(dim_tuple)
                                        logger.info("Successfully created dimension for wall ID {}.".format(wall.Id))
                                    except OperationCanceledException:
                                        logger.warning("Dimension creation cancelled by user.")
                                        continue
                                    except Exception as e:
                                        logger.error("Failed to create dimension for wall ID {}: {}".format(wall.Id, str(e)))
                                        logger.error(traceback.format_exc())
                                        continue
                                else:
                                    logger.info("Duplicate dimension detected for wall ID {}. Skipping.".format(wall.Id))
                            else:
                                logger.info("Dimension line already exists for wall ID {}. Skipping.".format(wall.Id))
                    else:
                        logger.warning("Insufficient references to create dimension for wall ID {}.".format(wall.Id))
            except Exception as e:
                logger.error("Failed to create dimension for wall with ID {}: {}".format(wall.Id, str(e)))
                logger.error(traceback.format_exc())
                continue

# ============================================
# Main Execution Flow
# ============================================
def main():
    try:
        start_time = time.time()
        # Initialize configuration (if needed)
        config = Config()
        # Get user inputs
        user_inputs = get_user_input()
        if user_inputs is None:
            logger.warning("User cancelled the operation.")
            forms.alert("Operation cancelled by the user.", exitscript=True)
            return

        selected_option, selected_level, selected_face, offset_distance, selected_dimensioning_type = user_inputs

        # Start Revit transaction
        with DB.Transaction(doc, "Auto Dimension Walls") as transaction:
            try:
                transaction.Start()
                create_wall_dimensions(
                    doc,
                    selected_option,
                    selected_level,
                    selected_face,
                    offset_distance,
                    selected_dimensioning_type,
                )
                transaction.Commit()
                logger.info("Transaction committed successfully.")
            except OperationCanceledException:
                transaction.RollBack()
                logger.warning("Transaction cancelled by user.")
                forms.alert("Transaction cancelled by user.", exitscript=True)
                return
            except Exception as e:
                transaction.RollBack()
                logger.error("An error occurred during the transaction: {}".format(str(e)))
                logger.error(traceback.format_exc())
                TaskDialog.Show("Error", "An error occurred while dimensioning walls:\n{}".format(str(e)))
                return

        end_time = time.time()
        elapsed_time = end_time - start_time
        summary_message = (
            "Dimensioning completed.\n\n"
            "Check the logs for detailed information.\n"
            "Time taken: {:.2f} seconds".format(elapsed_time)
        )
        logger.info(summary_message)
        TaskDialog.Show("Summary", summary_message)

    except Exception as e:
        logger.error("An unexpected error occurred: {}".format(str(e)))
        logger.error(traceback.format_exc())
        TaskDialog.Show("Error", "An unexpected error occurred. Please check the log for details.")
        raise

if __name__ == "__main__":
    main()

1 Like