# -----------------------------------------------------------------------------
# Cantilever Level 4 Beams 10' South of Grid F  -  Dynamo 3.x (CPython3 / PythonNet3)
#
# Finds every beam at Level 4 that runs perpendicular to Grid F and ends on
# it, and extends that end 10'-0" past Grid F to the exterior. Each beam
# becomes continuous over the Grid F column: 20' back-span (D-F) + 10'
# cantilever. Beams running along Grid F are left as they are.
#
#   With the current grid that's 4 beams, on grids 1, 2, 3 and 5:
#
#        1          2              3          5
#   D  --+----------+--------------+----------+--   y = 20'
#        |          |              |          |
#   F  --+----------+--------------+----------+--   y =  0'
#        |          |              |          |
#        +          +              +          +     y = -10'  (cantilever tips)
#
# "Exterior" = the side of Grid F away from the center of the building,
# so this works whichever way the grid sits in the project.
#
# Safe to re-run: once extended, a beam no longer ends on Grid F, so it is
# not extended again.
#
# HOW TO USE
#   Dynamo 3.x > Python Script node (CPython3 or PythonNet3) > paste > Run.
#   Ctrl+Z in Revit undoes the whole run.
# -----------------------------------------------------------------------------

import clr
clr.AddReference("RevitAPI")
clr.AddReference("RevitServices")

from Autodesk.Revit.DB import (
    XYZ, Line, Grid, Level, LocationCurve, ElementId,
    FilteredElementCollector, BuiltInCategory, BuiltInParameter,
)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument

# ---------------------------------------------------------------- settings ---
LEVEL_NAME = "Level 4"
EDGE_GRID = "F"
CANTILEVER = 10.0     # feet past the grid
TOL = 0.01

levels = {l.Name: l for l in FilteredElementCollector(doc).OfClass(Level)}
if LEVEL_NAME not in levels:
    raise Exception("'{}' not found. Levels: {}".format(LEVEL_NAME, ", ".join(sorted(levels))))
level = levels[LEVEL_NAME]

grids = {g.Name: g for g in FilteredElementCollector(doc).OfClass(Grid)}
if EDGE_GRID not in grids or not isinstance(grids[EDGE_GRID].Curve, Line):
    raise Exception("Straight grid '{}' not found.".format(EDGE_GRID))


def flat(p):
    return XYZ(p.X, p.Y, 0)


g0 = flat(grids[EDGE_GRID].Curve.GetEndPoint(0))
g1 = flat(grids[EDGE_GRID].Curve.GetEndPoint(1))
g_dir = (g1 - g0).Normalize()


def dist_to_grid(p):
    v = flat(p) - g0
    return abs(v.X * g_dir.Y - v.Y * g_dir.X)


# Outward = perpendicular to Grid F, pointing away from the building center
# (center = average of all straight-grid intersections' bounding box).
xs, ys = [], []
for g in grids.values():
    if isinstance(g.Curve, Line):
        for i in (0, 1):
            p = g.Curve.GetEndPoint(i)
            xs.append(p.X)
            ys.append(p.Y)
center = XYZ((min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0, 0)
normal = XYZ(-g_dir.Y, g_dir.X, 0)
if (center - g0).DotProduct(normal) > 0:
    normal = normal.Negate()


def ref_level(e):
    p = e.get_Parameter(BuiltInParameter.INSTANCE_REFERENCE_LEVEL_PARAM)
    return p.AsElementId() if p else ElementId.InvalidElementId


beams = [b for b in FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_StructuralFraming)
         .WhereElementIsNotElementType()
         if ref_level(b) == level.Id and isinstance(b.Location, LocationCurve)
         and isinstance(b.Location.Curve, Line)]

report = []
extended = 0

TransactionManager.Instance.EnsureInTransaction(doc)
try:
    for b in beams:
        c = b.Location.Curve
        p0, p1 = c.GetEndPoint(0), c.GetEndPoint(1)
        d = flat(p1 - p0)
        if d.GetLength() < TOL:
            continue
        d = d.Normalize()
        if abs(d.DotProduct(g_dir)) > 0.1:
            continue  # runs along Grid F (or skewed) - not a cantilever candidate

        on0, on1 = dist_to_grid(p0) < TOL, dist_to_grid(p1) < TOL
        if on0 == on1:
            continue  # neither end (or both) on Grid F

        if on1:
            out = (p1 - p0).Normalize()
            if flat(out).DotProduct(normal) <= 0:
                continue  # beam lies outside F pointing in - leave it
            new_line = Line.CreateBound(p0, p1 + out.Multiply(CANTILEVER))
        else:
            out = (p0 - p1).Normalize()
            if flat(out).DotProduct(normal) <= 0:
                continue
            new_line = Line.CreateBound(p0 + out.Multiply(CANTILEVER), p1)

        b.Location.Curve = new_line
        extended += 1
        report.append("Extended beam {} ({}) to {:.0f}' long".format(
            b.Id, b.Name, new_line.Length))
finally:
    TransactionManager.Instance.TransactionTaskDone()

report.insert(0, "{} beam(s) at {} cantilevered {:.0f}' past Grid {}".format(
    extended, LEVEL_NAME, CANTILEVER, EDGE_GRID))
if extended == 0:
    report.append("Nothing changed - no {} beams end on Grid {} (already extended?).".format(
        LEVEL_NAME, EDGE_GRID))

OUT = report
