# -*- coding: utf-8 -*-
# Sun-responsive fins: builds a "Sun Fin" family, places it every 5' along the
# walls, and rotates each instance based on a view's Sun Settings.
# IronPython 2.7 (Dynamo 2.x). Revit 2022-2024.
import math
import os
import clr
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType
clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *
doc = DocumentManager.Instance.CurrentDBDocument
# ------------------------------------------------------------------
# SETTINGS (lengths in feet, angles in degrees)
# ------------------------------------------------------------------
# Building / fin geometry - same as the earlier scripts
LENGTH_FT = 100.0
WIDTH_FT = 40.0
FIN_SPACING_FT = 5.0
FIN_DEPTH_FT = 4.0 # projection from the wall
FIN_THICKNESS_FT = 0.5
LEVEL_1_NAME = "L1"
LEVEL_2_NAME = "L2"
# Family handling
FAMILY_NAME = "Sun Fin"
REBUILD_FAMILY = False # True = rebuild and reload the family (use after changing size settings)
RECREATE_FINS = False # True = delete and re-place all fins instead of just rotating them
REMOVE_OLD_DIRECTSHAPE_FINS = True # delete the solid fins made by the earlier script ("Fin")
OLD_FIN_NAME = "Fin"
# Sun source
VIEW_NAME = None # None = active view; or e.g. "3D View: Sun Study"
MANUAL_AZIMUTH_DEG = None # set both of these to ignore the view and use your own sun position
MANUAL_ALTITUDE_DEG = None # azimuth: 0 = north, 90 = east, 180 = south, 270 = west
AZIMUTH_RELATIVE_TO = "TRUE_NORTH" # or "PROJECT_NORTH" if the sun looks rotated wrongly
TRUE_NORTH_ANGLE_DEG = None # None = read from project
# Optimization
MAX_ROTATION_DEG = 75.0 # fins may swing this far either side of perpendicular
ANGLE_STEP_DEG = 1.0
DIRECT_WEIGHT = 1.0 # higher = block direct sun more aggressively
# ------------------------------------------------------------------
# Walls: unit vector along the wall (u) and outward normal (n). Same layout as before.
WALLS = [
("South", (1.0, 0.0), (0.0, -1.0)),
("East", (0.0, 1.0), (1.0, 0.0)),
("North", (-1.0, 0.0), (0.0, 1.0)),
("West", (0.0, -1.0), (-1.0, 0.0)),
]
# Fin placement: (name, start, end, include_end_points) - long walls own the corners
PLACEMENT = [
("South", (0.0, 0.0), (LENGTH_FT, 0.0), True),
("East", (LENGTH_FT, 0.0), (LENGTH_FT, WIDTH_FT), False),
("North", (LENGTH_FT, WIDTH_FT), (0.0, WIDTH_FT), True),
("West", (0.0, WIDTH_FT), (0.0, 0.0), False),
]
def dot(a, b):
return a[0] * b[0] + a[1] * b[1]
# ---------------- helpers: levels, views, sun ----------------
def find_level(name):
for lvl in FilteredElementCollector(doc).OfClass(Level):
if lvl.Name == name:
return lvl
return None
def get_view(name):
if name is None:
return doc.ActiveView
for v in FilteredElementCollector(doc).OfClass(View):
if v.Name == name and not v.IsTemplate:
return v
raise Exception("View '%s' not found." % name)
def get_sun_from_view(view):
"""Return (azimuth, altitude) in radians from the view's Sun Settings."""
ss = view.SunAndShadowSettings
if ss is None:
raise Exception("View '%s' has no sun settings. Use a plan, elevation or 3D view "
"(set VIEW_NAME), or fill in MANUAL_AZIMUTH_DEG / MANUAL_ALTITUDE_DEG."
% view.Name)
try:
frame = ss.ActiveFrame
return ss.GetFrameAzimuth(frame), ss.GetFrameAltitude(frame)
except Exception:
return ss.Azimuth, ss.Altitude
# ---------------- fin light-transmission model ----------------
def transmit(phi, theta):
"""Fraction of light from direction phi (angle from the wall normal) that passes
between fins turned by theta. Fin thickness ignored."""
v = 1.0 - (FIN_DEPTH_FT * abs(math.sin(phi - theta)) /
(FIN_SPACING_FT * math.cos(phi)))
return v if v > 0.0 else 0.0
def best_theta(rays, diffuse_dirs):
"""Return (theta_deg, direct_pct, diffuse_pct, direct_pct_perp, diffuse_pct_perp)."""
sum_w = sum([w for (p, w) in rays])
sum_c = sum([c for (p, c) in diffuse_dirs])
def evaluate(theta_deg):
th = math.radians(theta_deg)
direct = 0.0
if sum_w > 0.0:
direct = sum([w * transmit(p, th) for (p, w) in rays]) / sum_w
diffuse = sum([c * transmit(p, th) for (p, c) in diffuse_dirs]) / sum_c
return direct, diffuse
best = None
t = -MAX_ROTATION_DEG
while t <= MAX_ROTATION_DEG + 1e-9:
direct, diffuse = evaluate(t)
score = diffuse - DIRECT_WEIGHT * direct
if (best is None or score > best[0] + 1e-9 or
(abs(score - best[0]) <= 1e-9 and abs(t) < abs(best[1]))):
best = (score, t, direct, diffuse)
t += ANGLE_STEP_DEG
d0, f0 = evaluate(0.0)
return best[1], best[2] * 100, best[3] * 100, d0 * 100, f0 * 100
# ---------------- family creation ----------------
class FamLoadOptions(IFamilyLoadOptions):
"""Lets Revit overwrite the family if it is already in the project."""
def OnFamilyFound(self, familyInUse, overwriteParameterValues):
overwriteParameterValues.Value = True
return True
def OnSharedFamilyFound(self, sharedFamily, familyInUse, source, overwriteParameterValues):
source.Value = FamilySource.Family
overwriteParameterValues.Value = True
return True
def find_template(app):
"""Find the Generic Model family template installed with Revit."""
root = app.FamilyTemplatePath
wanted = ["generic model.rft", "metric generic model.rft"]
for base in (root, os.path.dirname(root)):
for folder, dirs, files in os.walk(base):
for f in files:
if f.lower() in wanted:
return os.path.join(folder, f)
raise Exception("Could not find 'Generic Model.rft' under: " + str(root))
def add_angle_parameter(fdoc):
"""Add an instance parameter 'Fin Angle' (informational - the script fills it in)."""
fm = fdoc.FamilyManager
try:
fm.AddParameter("Fin Angle", GroupTypeId.Geometry, SpecTypeId.Angle, True) # Revit 2022+
except Exception:
fm.AddParameter("Fin Angle", BuiltInParameterGroup.PG_GEOMETRY, ParameterType.Angle, True)
def build_and_load_family(height):
"""Make the fin family (fin extends from the origin along local +Y) and load it."""
app = doc.Application
fdoc = app.NewFamilyDocument(find_template(app))
try:
t = Transaction(fdoc, "Build sun fin family")
t.Start()
sp = SketchPlane.Create(fdoc, Plane.CreateByNormalAndOrigin(XYZ.BasisZ, XYZ.Zero))
h = FIN_THICKNESS_FT / 2.0
pts = [XYZ(-h, 0, 0), XYZ(h, 0, 0), XYZ(h, FIN_DEPTH_FT, 0), XYZ(-h, FIN_DEPTH_FT, 0)]
arr = CurveArray()
for i in range(4):
arr.Append(Line.CreateBound(pts[i], pts[(i + 1) % 4]))
profile = CurveArrArray()
profile.Append(arr)
fdoc.FamilyCreate.NewExtrusion(True, profile, sp, height)
try:
add_angle_parameter(fdoc)
except Exception:
pass # parameter is optional; rotation still works without it
t.Commit()
# Save under the family name (so it loads with the right name), then load
path = os.path.join(os.environ.get("TEMP", "C:\\Temp"), FAMILY_NAME + ".rfa")
so = SaveAsOptions()
so.OverwriteExistingFile = True
fdoc.SaveAs(path, so)
fdoc.LoadFamily(doc, FamLoadOptions())
finally:
fdoc.Close(False)
def find_symbol():
for s in FilteredElementCollector(doc).OfClass(FamilySymbol):
if s.Family.Name == FAMILY_NAME:
return s
return None
# ---------------- fin instances ----------------
def wall_of_point(pt):
"""Which wall a fin belongs to, from its location (corner fins belong to South/North)."""
tol = 0.5
if abs(pt.Y) < tol:
return "South"
if abs(pt.Y - WIDTH_FT) < tol:
return "North"
if abs(pt.X - LENGTH_FT) < tol:
return "East"
return "West"
def fin_instances():
out = []
for fi in FilteredElementCollector(doc).OfClass(FamilyInstance).ToElements():
if fi.Symbol.Family.Name == FAMILY_NAME:
out.append(fi)
return out
def set_rotation(inst, alpha, theta_deg):
"""Turn the instance to absolute rotation alpha (radians) and record the angle."""
lp = inst.Location
pt = lp.Point
delta = alpha - lp.Rotation
delta = (delta + math.pi) % (2.0 * math.pi) - math.pi
if abs(delta) > 1e-9:
lp.Rotate(Line.CreateBound(pt, XYZ(pt.X, pt.Y, pt.Z + 1.0)), delta)
prm = inst.LookupParameter("Fin Angle")
if prm is not None and not prm.IsReadOnly:
prm.Set(math.radians(theta_deg))
try:
report = []
level1 = find_level(LEVEL_1_NAME)
level2 = find_level(LEVEL_2_NAME)
if level1 is None or level2 is None:
raise Exception("Could not find levels '%s' and '%s'." % (LEVEL_1_NAME, LEVEL_2_NAME))
fin_height = 2.0 * (level2.Elevation - level1.Elevation)
# --- True north / east in project XY ---
if AZIMUTH_RELATIVE_TO == "PROJECT_NORTH":
north_ang = 0.0
elif TRUE_NORTH_ANGLE_DEG is not None:
north_ang = math.radians(TRUE_NORTH_ANGLE_DEG)
else:
north_ang = doc.ActiveProjectLocation.GetProjectPosition(XYZ.Zero).Angle
tn = (-math.sin(north_ang), math.cos(north_ang))
te = (math.cos(north_ang), math.sin(north_ang))
# --- Sun position ---
if MANUAL_AZIMUTH_DEG is not None and MANUAL_ALTITUDE_DEG is not None:
az = math.radians(MANUAL_AZIMUTH_DEG)
alt = math.radians(MANUAL_ALTITUDE_DEG)
source = "manual values"
else:
view = get_view(VIEW_NAME)
az, alt = get_sun_from_view(view)
source = "view '%s'" % view.Name
report.append("Sun from %s: azimuth %.1f deg, altitude %.1f deg (compare with Sun Settings)"
% (source, math.degrees(az) % 360.0, math.degrees(alt)))
# --- Best angle per wall for this one sun position ---
diffuse_dirs = []
d = -88.0
while d <= 88.0 + 1e-9:
p = math.radians(d)
diffuse_dirs.append((p, math.cos(p)))
d += 2.0
sh = (tn[0] * math.cos(az) + te[0] * math.sin(az),
tn[1] * math.cos(az) + te[1] * math.sin(az)) # horizontal direction to the sun
sun_up = alt > math.radians(1.0)
target = {} # wall -> (theta_deg, absolute instance rotation in radians)
for name, u, n in WALLS:
rays = []
if sun_up:
cphi = dot(sh, n)
if cphi > 0.05: # sun is in front of this wall
rays.append((math.atan2(dot(sh, u), cphi), 1.0))
theta, direct, diffuse, d0, f0 = best_theta(rays, diffuse_dirs)
th = math.radians(theta)
f = (n[0] * math.cos(th) + u[0] * math.sin(th),
n[1] * math.cos(th) + u[1] * math.sin(th)) # direction the fin points
alpha = math.atan2(-f[0], f[1]) # local +Y -> f
target[name] = (theta, alpha)
if rays:
report.append("%s wall: sun in front, fins turn %+.0f deg | direct sun %.0f%% (%.0f%% if perpendicular), "
"diffuse %.0f%% (%.0f%%)" % (name, theta, direct, d0, diffuse, f0))
else:
report.append("%s wall: no direct sun, fins stay perpendicular (0 deg)" % name)
if not sun_up:
report.append("Sun is below the horizon: all fins set perpendicular.")
# --- Model changes ---
TransactionManager.Instance.EnsureInTransaction(doc)
try:
# Family: build it once (or when asked to rebuild)
symbol = find_symbol()
if symbol is None or REBUILD_FAMILY:
build_and_load_family(fin_height)
symbol = find_symbol()
if symbol is None:
raise Exception("Family '%s' did not load." % FAMILY_NAME)
report.append("Built and loaded family '%s'" % FAMILY_NAME)
if not symbol.IsActive:
symbol.Activate()
doc.Regenerate()
# Remove the old solid fins from the earlier script
if REMOVE_OLD_DIRECTSHAPE_FINS:
old = [ds.Id for ds in FilteredElementCollector(doc).OfClass(DirectShape).ToElements()
if ds.Name == OLD_FIN_NAME]
for eid in old:
doc.Delete(eid)
if old:
report.append("Removed %d old solid fins" % len(old))
# Existing fin instances: keep and rotate, or delete and re-place
instances = fin_instances()
if RECREATE_FINS and instances:
for fi in instances:
doc.Delete(fi.Id)
instances = []
if len(instances) == 0:
for name, start, end, include_ends in PLACEMENT:
dx = end[0] - start[0]
dy = end[1] - start[1]
wall_len = (dx * dx + dy * dy) ** 0.5
ux, uy = dx / wall_len, dy / wall_len
i = 0
while i * FIN_SPACING_FT <= wall_len + 1e-9:
dist = i * FIN_SPACING_FT
at_end = dist < 1e-9 or abs(dist - wall_len) < 1e-9
if include_ends or not at_end:
pt = XYZ(start[0] + ux * dist, start[1] + uy * dist, level1.Elevation)
doc.Create.NewFamilyInstance(pt, symbol, level1, StructuralType.NonStructural)
i += 1
instances = fin_instances()
report.append("Placed %d fins" % len(instances))
# Rotate every fin to its wall's target angle
for fi in instances:
theta, alpha = target[wall_of_point(fi.Location.Point)]
set_rotation(fi, alpha, theta)
report.append("Updated rotation on %d fins" % len(instances))
TransactionManager.Instance.TransactionTaskDone()
except Exception as e:
TransactionManager.Instance.ForceCloseTransaction()
raise e
OUT = "\n".join(report)
except Exception as e:
import traceback
OUT = "ERROR: " + str(e) + "\n" + traceback.format_exc()
# -*- coding: utf-8 -*-
# Sun-responsive fins: builds a "Sun Fin" family, places it every 5' along the
# walls, and rotates each instance based on a view's STILL Sun Settings.
# IronPython 2.7 (Dynamo 2.x).
import math
import os
import clr
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType
clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *
doc = DocumentManager.Instance.CurrentDBDocument
# ------------------------------------------------------------------
# SETTINGS (lengths in feet, angles in degrees)
# ------------------------------------------------------------------
# Building / fin geometry - same as the earlier scripts
LENGTH_FT = 100.0
WIDTH_FT = 40.0
FIN_SPACING_FT = 5.0
FIN_DEPTH_FT = 4.0 # projection from the wall
FIN_THICKNESS_FT = 0.5
LEVEL_1_NAME = "L1"
LEVEL_2_NAME = "L2"
# Family handling
FAMILY_NAME = "Sun Fin"
REBUILD_FAMILY = False # True = delete and rebuild the family (use after changing size settings)
RECREATE_FINS = False # True = delete and re-place all fins instead of just rotating them
REMOVE_OLD_DIRECTSHAPE_FINS = True # delete the solid fins made by the earlier script ("Fin")
OLD_FIN_NAME = "Fin"
# Sun source: the Still setting in a view's Sun Settings dialog
VIEW_NAME = None # None = active view; or the exact name of a view, e.g. "3D View: Sun Study"
REQUIRE_STILL = True # True = stop with a clear message if the view's Solar Study isn't "Still"
AZIMUTH_RELATIVE_TO = "TRUE_NORTH" # or "PROJECT_NORTH" if the fins turn as if the sun were rotated
TRUE_NORTH_ANGLE_DEG = None # None = read from project
# Optimization
MAX_ROTATION_DEG = 75.0 # fins may swing this far either side of perpendicular
ANGLE_STEP_DEG = 1.0
DIRECT_WEIGHT = 1.0 # higher = block direct sun more aggressively
# ------------------------------------------------------------------
# Walls: unit vector along the wall (u) and outward normal (n). Same layout as before.
WALLS = [
("South", (1.0, 0.0), (0.0, -1.0)),
("East", (0.0, 1.0), (1.0, 0.0)),
("North", (-1.0, 0.0), (0.0, 1.0)),
("West", (0.0, -1.0), (-1.0, 0.0)),
]
# Fin placement: (name, start, end, include_end_points) - long walls own the corners
PLACEMENT = [
("South", (0.0, 0.0), (LENGTH_FT, 0.0), True),
("East", (LENGTH_FT, 0.0), (LENGTH_FT, WIDTH_FT), False),
("North", (LENGTH_FT, WIDTH_FT), (0.0, WIDTH_FT), True),
("West", (0.0, WIDTH_FT), (0.0, 0.0), False),
]
def dot(a, b):
return a[0] * b[0] + a[1] * b[1]
# ---------------- helpers: levels, views, sun ----------------
def find_level(name):
for lvl in FilteredElementCollector(doc).OfClass(Level):
if lvl.Name == name:
return lvl
return None
def get_view(name):
if name is None:
return doc.ActiveView
for v in FilteredElementCollector(doc).OfClass(View):
if v.Name == name and not v.IsTemplate:
return v
raise Exception("View '%s' not found." % name)
def get_still_sun(view):
"""
Read the sun position from the view's Sun Settings (Solar Study = Still).
Returns (azimuth_radians, altitude_radians, date_time_text).
"""
ss = view.SunAndShadowSettings
if ss is None:
raise Exception("View '%s' has no Sun Settings. Use a plan, elevation or 3D view "
"(set VIEW_NAME to its exact name)." % view.Name)
study_type = ss.SunAndShadowType
if REQUIRE_STILL and study_type != SunAndShadowType.StillImage:
raise Exception("View '%s' has Solar Study type '%s'. Open Sun Settings (sun icon in the "
"View Control Bar), set Solar Study to 'Still', choose the date and time, "
"and run again." % (view.Name, str(study_type)))
when = ""
try:
when = str(ss.StartDateAndTime) # date/time shown in the Still dialog
except Exception:
pass
return ss.Azimuth, ss.Altitude, when
# ---------------- fin light-transmission model ----------------
def transmit(phi, theta):
"""Fraction of light from direction phi (angle from the wall normal) that passes
between fins turned by theta. Fin thickness ignored."""
v = 1.0 - (FIN_DEPTH_FT * abs(math.sin(phi - theta)) /
(FIN_SPACING_FT * math.cos(phi)))
return v if v > 0.0 else 0.0
def best_theta(rays, diffuse_dirs):
"""Return (theta_deg, direct_pct, diffuse_pct, direct_pct_perp, diffuse_pct_perp)."""
sum_w = sum([w for (p, w) in rays])
sum_c = sum([c for (p, c) in diffuse_dirs])
def evaluate(theta_deg):
th = math.radians(theta_deg)
direct = 0.0
if sum_w > 0.0:
direct = sum([w * transmit(p, th) for (p, w) in rays]) / sum_w
diffuse = sum([c * transmit(p, th) for (p, c) in diffuse_dirs]) / sum_c
return direct, diffuse
best = None
t = -MAX_ROTATION_DEG
while t <= MAX_ROTATION_DEG + 1e-9:
direct, diffuse = evaluate(t)
score = diffuse - DIRECT_WEIGHT * direct
if (best is None or score > best[0] + 1e-9 or
(abs(score - best[0]) <= 1e-9 and abs(t) < abs(best[1]))):
best = (score, t, direct, diffuse)
t += ANGLE_STEP_DEG
d0, f0 = evaluate(0.0)
return best[1], best[2] * 100, best[3] * 100, d0 * 100, f0 * 100
# ---------------- family creation ----------------
def find_template(app):
"""Find the Generic Model family template installed with Revit."""
root = app.FamilyTemplatePath
wanted = ["generic model.rft", "metric generic model.rft"]
for base in (root, os.path.dirname(root)):
for folder, dirs, files in os.walk(base):
for f in files:
if f.lower() in wanted:
return os.path.join(folder, f)
raise Exception("Could not find 'Generic Model.rft' under: " + str(root))
def add_angle_parameter(fdoc):
"""Add an instance parameter 'Fin Angle' (informational - the script fills it in)."""
fm = fdoc.FamilyManager
try:
fm.AddParameter("Fin Angle", GroupTypeId.Geometry, SpecTypeId.Angle, True) # Revit 2022+
except Exception:
fm.AddParameter("Fin Angle", BuiltInParameterGroup.PG_GEOMETRY, ParameterType.Angle, True)
def delete_family(fam):
"""Delete a family (and its instances) in its own short transaction."""
fam_id = fam.Id
t = Transaction(doc, "Delete old Sun Fin family")
t.Start()
try:
doc.Delete(fam_id)
t.Commit()
except Exception as e:
t.RollBack()
raise e
def build_and_load_family(height):
"""Make the fin family (fin extends from the origin along local +Y) and load it.
Must be called while the project has NO open transaction."""
app = doc.Application
fdoc = app.NewFamilyDocument(find_template(app))
try:
t = Transaction(fdoc, "Build sun fin family")
t.Start()
sp = SketchPlane.Create(fdoc, Plane.CreateByNormalAndOrigin(XYZ.BasisZ, XYZ.Zero))
h = FIN_THICKNESS_FT / 2.0
pts = [XYZ(-h, 0, 0), XYZ(h, 0, 0), XYZ(h, FIN_DEPTH_FT, 0), XYZ(-h, FIN_DEPTH_FT, 0)]
arr = CurveArray()
for i in range(4):
arr.Append(Line.CreateBound(pts[i], pts[(i + 1) % 4]))
profile = CurveArrArray()
profile.Append(arr)
fdoc.FamilyCreate.NewExtrusion(True, profile, sp, height)
try:
add_angle_parameter(fdoc)
except Exception:
pass # parameter is optional; rotation still works without it
t.Commit()
# Save under the family name (so it loads with the right name), then load.
# Any existing copy was already deleted by the caller.
path = os.path.join(os.environ.get("TEMP", "C:\\Temp"), FAMILY_NAME + ".rfa")
so = SaveAsOptions()
so.OverwriteExistingFile = True
fdoc.SaveAs(path, so)
fdoc.LoadFamily(doc)
finally:
fdoc.Close(False)
def find_symbol():
for s in FilteredElementCollector(doc).OfClass(FamilySymbol):
if s.Family.Name == FAMILY_NAME:
return s
return None
# ---------------- fin instances ----------------
def wall_of_point(pt):
"""Which wall a fin belongs to, from its location (corner fins belong to South/North)."""
tol = 0.5
if abs(pt.Y) < tol:
return "South"
if abs(pt.Y - WIDTH_FT) < tol:
return "North"
if abs(pt.X - LENGTH_FT) < tol:
return "East"
return "West"
def fin_instances():
out = []
for fi in FilteredElementCollector(doc).OfClass(FamilyInstance).ToElements():
if fi.Symbol.Family.Name == FAMILY_NAME:
out.append(fi)
return out
def set_rotation(inst, alpha, theta_deg):
"""Turn the instance to absolute rotation alpha (radians) and record the angle."""
lp = inst.Location
pt = lp.Point
delta = alpha - lp.Rotation
delta = (delta + math.pi) % (2.0 * math.pi) - math.pi
if abs(delta) > 1e-9:
lp.Rotate(Line.CreateBound(pt, XYZ(pt.X, pt.Y, pt.Z + 1.0)), delta)
prm = inst.LookupParameter("Fin Angle")
if prm is not None and not prm.IsReadOnly:
prm.Set(math.radians(theta_deg))
try:
report = []
level1 = find_level(LEVEL_1_NAME)
level2 = find_level(LEVEL_2_NAME)
if level1 is None or level2 is None:
raise Exception("Could not find levels '%s' and '%s'." % (LEVEL_1_NAME, LEVEL_2_NAME))
fin_height = 2.0 * (level2.Elevation - level1.Elevation)
# --- True north / east in project XY ---
if AZIMUTH_RELATIVE_TO == "PROJECT_NORTH":
north_ang = 0.0
elif TRUE_NORTH_ANGLE_DEG is not None:
north_ang = math.radians(TRUE_NORTH_ANGLE_DEG)
else:
north_ang = doc.ActiveProjectLocation.GetProjectPosition(XYZ.Zero).Angle
tn = (-math.sin(north_ang), math.cos(north_ang))
te = (math.cos(north_ang), math.sin(north_ang))
# --- Sun position from the view's Still Sun Settings ---
view = get_view(VIEW_NAME)
az, alt, when = get_still_sun(view)
report.append("Sun from Still settings in view '%s'%s: azimuth %.1f deg, altitude %.1f deg"
% (view.Name, (" (" + when + ")") if when else "",
math.degrees(az) % 360.0, math.degrees(alt)))
# --- Best angle per wall for this one sun position ---
diffuse_dirs = []
d = -88.0
while d <= 88.0 + 1e-9:
p = math.radians(d)
diffuse_dirs.append((p, math.cos(p)))
d += 2.0
sh = (tn[0] * math.cos(az) + te[0] * math.sin(az),
tn[1] * math.cos(az) + te[1] * math.sin(az)) # horizontal direction to the sun
sun_up = alt > math.radians(1.0)
target = {} # wall -> (theta_deg, absolute instance rotation in radians)
for name, u, n in WALLS:
rays = []
if sun_up:
cphi = dot(sh, n)
if cphi > 0.05: # sun is in front of this wall
rays.append((math.atan2(dot(sh, u), cphi), 1.0))
theta, direct, diffuse, d0, f0 = best_theta(rays, diffuse_dirs)
th = math.radians(theta)
f = (n[0] * math.cos(th) + u[0] * math.sin(th),
n[1] * math.cos(th) + u[1] * math.sin(th)) # direction the fin points
alpha = math.atan2(-f[0], f[1]) # local +Y -> f
target[name] = (theta, alpha)
if rays:
report.append("%s wall: sun in front, fins turn %+.0f deg | direct sun %.0f%% (%.0f%% if perpendicular), "
"diffuse %.0f%% (%.0f%%)" % (name, theta, direct, d0, diffuse, f0))
else:
report.append("%s wall: no direct sun, fins stay perpendicular (0 deg)" % name)
if not sun_up:
report.append("Sun is below the horizon: all fins set perpendicular.")
# --- Family step: runs BEFORE the main transaction, because Revit will not
# load a family while the project has an open transaction ---
symbol = find_symbol()
if symbol is None or REBUILD_FAMILY:
if symbol is not None:
delete_family(symbol.Family) # own short transaction, committed here
build_and_load_family(fin_height) # no transaction open on the project
symbol = find_symbol()
if symbol is None:
raise Exception("Family '%s' did not load." % FAMILY_NAME)
report.append("Built and loaded family '%s'" % FAMILY_NAME)
# --- Model changes (one transaction) ---
TransactionManager.Instance.EnsureInTransaction(doc)
try:
if not symbol.IsActive:
symbol.Activate()
doc.Regenerate()
# Remove the old solid fins from the earlier script
if REMOVE_OLD_DIRECTSHAPE_FINS:
old = [ds.Id for ds in FilteredElementCollector(doc).OfClass(DirectShape).ToElements()
if ds.Name == OLD_FIN_NAME]
for eid in old:
doc.Delete(eid)
if old:
report.append("Removed %d old solid fins" % len(old))
# Existing fin instances: keep and rotate, or delete and re-place
instances = fin_instances()
if RECREATE_FINS and instances:
for fi in instances:
doc.Delete(fi.Id)
instances = []
if len(instances) == 0:
for name, start, end, include_ends in PLACEMENT:
dx = end[0] - start[0]
dy = end[1] - start[1]
wall_len = (dx * dx + dy * dy) ** 0.5
ux, uy = dx / wall_len, dy / wall_len
i = 0
while i * FIN_SPACING_FT <= wall_len + 1e-9:
dist = i * FIN_SPACING_FT
at_end = dist < 1e-9 or abs(dist - wall_len) < 1e-9
if include_ends or not at_end:
pt = XYZ(start[0] + ux * dist, start[1] + uy * dist, level1.Elevation)
doc.Create.NewFamilyInstance(pt, symbol, level1, StructuralType.NonStructural)
i += 1
instances = fin_instances()
report.append("Placed %d fins" % len(instances))
# Rotate every fin to its wall's target angle
for fi in instances:
theta, alpha = target[wall_of_point(fi.Location.Point)]
set_rotation(fi, alpha, theta)
report.append("Updated rotation on %d fins" % len(instances))
TransactionManager.Instance.TransactionTaskDone()
except Exception as e:
TransactionManager.Instance.ForceCloseTransaction()
raise e
OUT = "\n".join(report)
except Exception as e:
import traceback
OUT = "ERROR: " + str(e) + "\n" + traceback.format_exc()