Skip to content

Automation Section view of Curtain walls #3221

@noahsmit80-ui

Description

@noahsmit80-ui

When running my Dynamo script in Revit, it creates sections based on the Type Mark parameter. The problem is that if I accidentally delete one of these sections and then rerun the script, Dynamo does not recreate the deleted section. Even though it has been removed from the model, Dynamo still seems to think it exists and therefore skips generating it again.

This behavior happens because Dynamo typically checks for existing elements (by ID or parameter) and assumes they are still valid, even if they were deleted in Revit. As a result, the script does not regenerate the missing section, leaving gaps in the output.

What I actually want is for Dynamo to recreate sections that are missing when I rerun the script — so if a section with a given Type Mark is no longer in the model, Dynamo should generate it again.

# -*- coding: utf-8 -*-
# SCRIPT 1 — CREATE
# Front elevations (Section Views as elevation) for CURTAIN WALLS with 'H' in Mark
# Looks for:
#  - INSTANCE Mark (Identity Data)
#  - (fallback) Type Mark / Type Name
#  - (optional) Revit Links (coordinates transformed)
# ALWAYS creates new sections per run (unique name with timestamp),
# sets Discipline/Phase correctly, and opens the first created view immediately.
# Author: Copilot for Noah Smit

import clr
import System
from System import DateTime

# Dynamo/Revit
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import (
    FilteredElementCollector, BuiltInParameter,
    ViewFamily, ViewFamilyType, ViewSection, View,
    BoundingBoxXYZ, Transform, XYZ, Wall, WallType,
    ElementId, ViewDiscipline, RevitLinkInstance
)

doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument

# ---------- CONSTANTS ----------
FILTER_TEXT        = "H"       # Looks for 'H' (case-insensitive)
MARGIN_MM          = 100.0
DEPTH_MM           = 500.0
OFFSET_MM          = 100.0
NAME_PREFIX        = "CW-AZ"   # Use the same prefix in Script 2 (delete)
INCLUDE_LINKS      = True      # Also search in Revit Links
USE_TYPE_FALLBACK  = True      # If instance Mark is empty: use Type Mark/Name
DEBUG              = False     # True => OUT contains debug rows

# ---------- Conversion ----------
MM_TO_FT = 1.0 / 304.8
margin = MARGIN_MM * MM_TO_FT
depth  = DEPTH_MM  * MM_TO_FT
offset = OFFSET_MM * MM_TO_FT

# ---------- Helpers ----------
def normalize(v):
    try: return v.Normalize()
    except: return v

def is_curtain_wall(w):
    try:
        wt = w.WallType
        if not isinstance(wt, WallType): return False
        try:
            from Autodesk.Revit.DB import WallKind
            return wt.Kind == WallKind.Curtain
        except:
            return hasattr(w, "CurtainGrid") and w.CurtainGrid is not None
    except:
        return False

def str_or_none(p):
    if not p or not p.HasValue: return None
    return p.AsString() or p.AsValueString()

def get_instance_mark(elem):
    # Identity Data (instance)
    v = str_or_none(elem.get_Parameter(BuiltInParameter.ALL_MODEL_MARK))
    if v: return (str(v), "Instance Mark")
    for nm in ("Mark", "Merk"):
        try:
            p = elem.LookupParameter(nm)
            v = str_or_none(p)
            if v: return (str(v), "Instance {}".format(nm))
        except: pass
    return (None, None)

def get_type_mark_or_name(d, elem):
    try:
        t = d.GetElement(elem.GetTypeId())
        if t:
            v = str_or_none(t.get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_MARK))
            if v: return (str(v), "Type Mark")
            for nm in ("Type Mark", "Type Merk"):
                try:
                    p = t.LookupParameter(nm)
                    v = str_or_none(p)
                    if v: return (str(v), "Type {}".format(nm))
                except: pass
            return (str(t.Name), "Type Name")
    except: pass
    return (None, None)

def contains_filter(val, needle):
    return (val is not None) and (needle.lower() in str(val).lower())

def wall_midpoint_and_tangent(w):
    loc = w.Location
    if not loc: return (None, None)
    crv = getattr(loc, "Curve", None)
    if not crv: return (None, None)
    try:
        deriv = crv.ComputeDerivatives(0.5, True)
        return (deriv.Origin, normalize(deriv.BasisX))
    except:
        p0 = crv.GetEndPoint(0); p1 = crv.GetEndPoint(1)
        mid = XYZ((p0.X+p1.X)*0.5, (p0.Y+p1.Y)*0.5, (p0.Z+p1.Z)*0.5)
        tan = normalize(p1 - p0)
        return (mid, tan)

def wall_length_ft(w):
    try:
        crv = w.Location.Curve
        if crv: return float(crv.Length)
    except: pass
    return 3000.0 * MM_TO_FT

def wall_height_ft(w):
    try:
        p = w.get_Parameter(BuiltInParameter.WALL_USER_HEIGHT_PARAM)
        if p and p.HasValue and p.AsDouble() and p.AsDouble() > 0:
            return float(p.AsDouble())
    except: pass
    try:
        bb = w.get_BoundingBox(None)
        if bb: return float(bb.Max.Z - bb.Min.Z)
    except: pass
    return 3000.0 * MM_TO_FT

def get_section_vft(d):
    for vft in FilteredElementCollector(d).OfClass(ViewFamilyType):
        try:
            if vft.ViewFamily == ViewFamily.Section:
                return vft
        except: continue
    return None

def unique_timestamp():
    n = DateTime.Now
    return "{:04d}{:02d}{:02d}-{:02d}{:02d}{:02d}-{:03d}".format(
        n.Year, n.Month, n.Day, n.Hour, n.Minute, n.Second, n.Millisecond
    )

# ---------- Collect targets (host + optional links) ----------
sect_vft = get_section_vft(doc)
if not sect_vft:
    OUT = "❌ No Section ViewFamilyType found. Add one (Manage > Transfer Project Standards > View Types)."
else:
    targets = []  # (wallElem, markVal, linkTransformOrNone)
    debug_rows = []

    # Host
    for w in FilteredElementCollector(doc).OfClass(Wall).ToElements():
        if not is_curtain_wall(w): continue
        mval, msrc = get_instance_mark(w)
        if (mval is None) and USE_TYPE_FALLBACK:
            mval, msrc = get_type_mark_or_name(doc, w)
        if DEBUG: debug_rows.append(["HOST", w.Id.IntegerValue, mval, msrc])
        if contains_filter(mval, FILTER_TEXT):
            targets.append((w, mval, None))

    # Links
    if INCLUDE_LINKS:
        for linst in FilteredElementCollector(doc).OfClass(RevitLinkInstance).ToElements():
            try:
                ldoc = linst.GetLinkDocument()
            except:
                ldoc = None
            if ldoc is None: continue
            tr = linst.GetTotalTransform()  # link->host
            for w in FilteredElementCollector(ldoc).OfClass(Wall).ToElements():
                if not is_curtain_wall(w): continue
                # Instance
                mv = str_or_none(w.get_Parameter(BuiltInParameter.ALL_MODEL_MARK))
                src = "Instance Mark (Link)" if mv else None
                if mv is None:
                    for nm in ("Mark","Merk"):
                        try:
                            p2 = w.LookupParameter(nm)
                            mv = str_or_none(p2)
                            if mv:
                                src = "Instance {} (Link)".format(nm); break
                        except: pass
                # Type fallback
                if (mv is None) and USE_TYPE_FALLBACK:
                    try:
                        t = ldoc.GetElement(w.GetTypeId())
                        if t:
                            tv = str_or_none(t.get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_MARK))
                            if tv:
                                mv = tv; src = "Type Mark (Link)"
                            if mv is None:
                                for nm in ("Type Mark","Type Merk"):
                                    p3 = t.LookupParameter(nm)
                                    tv = str_or_none(p3)
                                    if tv:
                                        mv = tv; src = "Type {} (Link)".format(nm); break
                            if mv is None:
                                mv = t.Name; src = "Type Name (Link)"
                    except: pass
                if DEBUG: debug_rows.append(["LINK:"+linst.Name, w.Id.IntegerValue, mv, src])
                if contains_filter(mv, FILTER_TEXT):
                    targets.append((w, mv, tr))

    if not targets:
        OUT = debug_rows if DEBUG else "ℹ️ No Curtain Walls found containing '{}' (Instance{}{}).".format(
            FILTER_TEXT,
            " + Type" if USE_TYPE_FALLBACK else "",
            " + Links" if INCLUDE_LINKS else ""
        )
    else:
        created = []
        ts = unique_timestamp()

        TransactionManager.Instance.EnsureInTransaction(doc)

        for (w, mark_val, tr_link2host) in targets:
            mid, tan = wall_midpoint_and_tangent(w)
            if not mid or not tan: continue

            # Front: towards the wall (exterior is +Orientation) -> view_dir = -Orientation
            try:
                view_dir = -normalize(w.Orientation)
            except:
                view_dir = normalize(XYZ.BasisZ.CrossProduct(tan))
            up = XYZ.BasisZ

            # From a link? transform to host
            if tr_link2host:
                mid  = tr_link2host.OfPoint(mid)
                tan  = normalize(tr_link2host.OfVector(tan))
                view_dir = normalize(tr_link2host.OfVector(view_dir))
                up   = normalize(tr_link2host.OfVector(up))

            right = normalize(up.CrossProduct(view_dir))

            half_w = 0.5 * wall_length_ft(w) + margin
            half_h = 0.5 * wall_height_ft(w) + margin

            origin = XYZ(mid.X - view_dir.X * offset,
                         mid.Y - view_dir.Y * offset,
                         mid.Z - view_dir.Z * offset)

            t = Transform.Identity
            t.Origin = origin
            t.BasisX = right
            t.BasisY = up
            t.BasisZ = view_dir

            box = BoundingBoxXYZ()
            box.Transform = t
            # 0..depth => elevation (not section cut)
            box.Min = XYZ(-half_w, -half_h, 0.0)
            box.Max = XYZ( half_w,  half_h, depth)

            v = None
            try:
                v = ViewSection.CreateSection(doc, sect_vft.Id, box)
            except:
                v = None

            if v:
                # Unlink template, set discipline & phase, and give unique name
                try:
                    if v.ViewTemplateId and v.ViewTemplateId != ElementId.InvalidElementId:
                        v.ViewTemplateId = ElementId.InvalidElementId
                except: pass
                try:
                    v.Discipline = ViewDiscipline.Coordination
                except: pass
                try:
                    ph = w.get_Parameter(BuiltInParameter.PHASE_CREATED)
                    if ph and ph.HasValue:
                        phId = ph.AsElementId()
                        vp = v.get_Parameter(BuiltInParameter.VIEW_PHASE)
                        if phId and vp: vp.Set(phId)
                except: pass

                safe_mark = (mark_val or "").strip()
                v.Name = "{}-{}-CW-{}-{}".format(NAME_PREFIX, safe_mark, w.Id.IntegerValue, ts)
                created.append([v.Id.IntegerValue, v.Name])

        TransactionManager.Instance.TransactionTaskDone()

        # Open the first new view (proof it exists)
        try:
            if created:
                first_view_id = ElementId(created[0][0])
                v_open = doc.GetElement(first_view_id)
                if v_open:
                    uidoc.ActiveView = v_open
        except: pass

        OUT = created if created else "⚠️ Matches found, but creating/naming views failed."

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Done

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions