Update the script to create parametric components that respond to the Revit sun position by rotating the fins.
‣
Response
1. What this script does
It builds a loadable Revit family, "Sun Fin", and places one instance every 5' around the building, replacing the earlier fins. It then reads the sun position from a Revit view's Sun Settings and turns each instance to the angle that blocks the most direct sun while keeping the most diffuse light. It uses the same wall-by-wall method as the last script, but for one sun position instead of a whole-year average.
It modifies the model, and Undo (Ctrl+Z) reverts it in one step. I can't run Revit scripts from here, so the family-building part in particular is untested. Please try it on a copy of the project first.
Three limits you should know about:
It isn't live. A Revit family can't read the sun position itself, so the fins only move when you run the script. After changing the time or date in Sun Settings, click Run again. A Dynamo Player shortcut makes that one click.
The fins turn by rotating each instance. Each fin also gets a "Fin Angle" instance parameter that the script fills in, so you can schedule it. Editing that parameter by hand won't turn the fin.
A fin whose geometry follows its own angle parameter needs reference planes and an angular dimension built in the Family Editor. That is fragile to automate. I can write step-by-step instructions for it if you want.
2. How to use it
Set the sun in your view first. In a plan, elevation or 3D view, turn on the sun path and open Sun Settings from the same menu. Pick the date and time you want to design for.
In Dynamo, place a Python Script node (not a Code Block) and paste the script below. There are no inputs.
Run it. The output shows the sun azimuth and altitude it read, and the angle chosen for each wall.
Check the printed azimuth and altitude against the Sun Settings dialog. If they don't match, the script is reading the wrong view (set VIEW_NAME) or the wrong north (see the tips).
After changing the sun settings, run it again to move the fins.
3. The script
4. Customization tips
Which sun to use: set VIEW_NAME to a specific view's name, or fill in MANUAL_AZIMUTH_DEG and MANUAL_ALTITUDE_DEG to test any sun position without touching Revit's settings. Azimuth is 0 for north, 90 for east, 180 for south and 270 for west.
Check the sun direction first: I'm not certain whether Revit measures the sun azimuth from true north or project north. Compare the printed azimuth with the Sun Settings dialog. If the fins turn as though the sun were rotated, set AZIMUTH_RELATIVE_TO = "PROJECT_NORTH".
Changing fin size: edit the geometry settings and set REBUILD_FAMILY = True and RECREATE_FINS = True for one run. Change them back afterwards.
If the family step fails: the script is built for Revit 2022-2024 and looks for the Generic Model template in Revit's template folder. If it reports a missing template or a failed load, paste me the error message. A fallback is to load any simple fin family yourself, name it "Sun Fin", and the script will use it. It needs to extend along its local +Y axis from its origin and have an instance parameter named "Fin Angle" if you want that recorded.
Quick re-runs: save the graph and use Dynamo Player. Changing the time in Sun Settings and clicking Run then takes a couple of seconds.
Rewrite the script so that it uses the position of the sun from the "still sun settings" in order to get the same result,
‣
Response
Here is the full script, rewritten to read the sun from the Still setting. It checks that the view's Solar Study type is set to Still, then reads that view's azimuth and altitude, which Revit calculates from the location, date and time in the dialog. Everything else is unchanged from the last version.
To set it up, open the view you want to use and click the sun icon in the View Control Bar, then choose Sun Settings. Set Solar Study to Still, then set the date and time. The script reads whichever view is active, or the view named in VIEW_NAME.
What changed:
The manual azimuth and altitude settings are gone, and so is the animation-frame fallback. The sun now always comes from the view's Still settings.
If the view's Solar Study is set to Single Day, Multi-Day or Lighting Only, the script stops and tells you to switch it to Still. Set REQUIRE_STILL = False if you'd rather it read whatever the view holds anyway.
The first line of the output now shows the view name, the date and time from the dialog, and the azimuth and altitude it used. Compare these with Sun Settings to confirm it read the right sun.
Things to check:
Direction: I'm not sure whether Revit measures this azimuth from true north or from project north. If the fins turn as though the sun were on the wrong side, set AZIMUTH_RELATIVE_TO = "PROJECT_NORTH".
Date and time: I haven't verified that StartDateAndTime is the value shown in the Still dialog. If the printed date and time look wrong but the azimuth and altitude match the sun path in your view, the fins will still be correct.
Changing the sun: change the date or time in the Still dialog, then run the script again. The fins only move when the script runs.
Final Script after working through errors:
Write a script that creates 4’ sun fins at 5’ intervals along the entire length of the walls. The script should be able to rotate the fins to minimize direct sun exposure while maximizing indirect sunlight. The script should take the azimuth and altitude as an input.
‣
Response and Result
1. What this script does
It builds the fins and rotates them from the sun position you give it. On a first run it:
creates a loadable family called “Sun Fin” (4' deep, 6" thick, 28' tall, from L1 up through both stories),
places one fin every 5' along all four walls, and
turns each wall’s fins to the angle that best blocks direct sun while keeping diffuse light.
On later runs it keeps the fins and only changes their rotation. The sun position comes from two inputs: IN[0] is the azimuth and IN[1] is the altitude. It modifies the model, and a first run needs two presses of Undo (Ctrl+Z), one for the fins and one for the family. I can’t run Revit from here, so this version is untested.
2. How to use it
Place a Python Script node (not a Code Block) and give it two input ports, using the + on the node.
Connect the azimuth to IN[0] and the altitude to IN[1], in degrees. Azimuth is measured clockwise from north: 0 is north, 90 is east, 180 is south and 270 is west. Altitude is the angle above the horizon.
To design for a whole day instead of one instant, connect a list of azimuths to IN[0] and an equal-length list of altitudes to IN[1]. The script optimizes the fins against all of those positions at once.
Run it. The first line of the output echoes what it received, followed by one line per wall explaining its angle.
3. The script
python
4. Customization tips
Why some walls stay at 0°: with a single sun position, only the wall facing the sun has a reason to turn its fins. Walls the sun doesn’t reach stay perpendicular, which also gives them the most diffuse light. For more walls to respond, feed in a whole day of positions as two lists.
Units and direction: if the altitude comes back as out of range, set INPUT_ANGLE_UNITS = "RADIANS". If the fins turn as though the sun were on the wrong side, set AZIMUTH_RELATIVE_TO = "PROJECT_NORTH".
Re-running: Dynamo only recalculates when an input changes. After changing the sun, move a slider or edit the number before running so the node actually executes.
Changing the fins: after editing the size settings, set REBUILD_FAMILY = True and RECREATE_FINS = True for one run, then set them back to False.
Different building: if you change the building’s size, update LENGTH_FT and WIDTH_FT to match.
Then, use the Dynamo blocks to connect to the inputs of the Python script:
Result:
# -*- 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()
# -*- coding: utf-8 -*-
# Sun fins: builds a "Sun Fin" family, places it every 5' along the walls of a
# 100' x 40' building, and rotates each fin using the sun position from the inputs.
# IN[0] = azimuth (degrees clockwise from north; a number or a list)
# IN[1] = altitude (degrees above the horizon; a number or a list)
# 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
# ------------------------------------------------------------------
# INPUTS
# ------------------------------------------------------------------
azimuth_input = IN[0]
altitude_input = IN[1]
# ------------------------------------------------------------------
# SETTINGS (lengths in feet, angles in degrees)
# ------------------------------------------------------------------
# Building / fin geometry
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 input handling
INPUT_ANGLE_UNITS = "DEGREES" # "DEGREES" or "RADIANS"
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
MIN_SUN_ALT_DEG = 2.0 # ignore sun positions lower than this
AFTERNOON_WEIGHT = 1.0 # >1 counts afternoon sun (azimuth > 180) as more important
# 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
MIN_EXPOSURE_FRACTION = 0.05 # a wall getting less than this share of the busiest wall's sun stays at 0
# ------------------------------------------------------------------
# Walls: unit vector along the wall (u) and outward normal (n).
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, sun input ----------------
def find_level(name):
for lvl in FilteredElementCollector(doc).OfClass(Level):
if lvl.Name == name:
return lvl
return None
def as_number_list(x, label):
"""Accept a number or a (possibly nested) list of numbers; return a flat list of floats."""
if x is None:
raise Exception("%s is empty. Connect a number (or a list of numbers) in degrees." % label)
if isinstance(x, (list, tuple)):
out = []
for item in x:
out.extend(as_number_list(item, label))
return out
try:
return [float(x)]
except Exception:
raise Exception("%s must be a number or a list of numbers (got %s)." % (label, type(x).__name__))
def read_sun_positions(az_in, alt_in):
"""Return a list of (azimuth_rad, altitude_rad)."""
azs = as_number_list(az_in, "IN[0] (azimuth)")
alts = as_number_list(alt_in, "IN[1] (altitude)")
if len(azs) == 0 or len(alts) == 0:
raise Exception("IN[0] and IN[1] must each contain at least one number.")
if len(azs) != len(alts):
if len(azs) == 1:
azs = azs * len(alts)
elif len(alts) == 1:
alts = alts * len(azs)
else:
raise Exception("IN[0] has %d values but IN[1] has %d. Give one azimuth and one altitude "
"per sun position (or a single value for either)." % (len(azs), len(alts)))
conv = 1.0 if INPUT_ANGLE_UNITS == "RADIANS" else math.pi / 180.0
return [(a * conv, b * conv) for (a, b) in zip(azs, alts)]
# ---------------- 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(s) from the inputs ---
positions = read_sun_positions(azimuth_input, altitude_input)
for (az, alt) in positions:
if abs(math.degrees(alt)) > 90.0 + 1e-6:
raise Exception("A sun altitude of %.1f deg is out of range (-90 to 90). "
"Check INPUT_ANGLE_UNITS." % math.degrees(alt))
# Each usable sun position -> (horizontal direction toward the sun, beam weight)
suns = []
for (az, alt) in positions:
if alt < math.radians(MIN_SUN_ALT_DEG):
continue
sin_alt = math.sin(alt)
dni = 0.7 ** ((1.0 / sin_alt) ** 0.678) # simple clear-sky beam strength
w = dni * math.cos(alt) # horizontal part, for vertical walls
if (math.degrees(az) % 360.0) > 180.0:
w *= AFTERNOON_WEIGHT
sh = (tn[0] * math.cos(az) + te[0] * math.sin(az),
tn[1] * math.cos(az) + te[1] * math.sin(az))
suns.append((sh, w))
if len(positions) == 1:
report.append("Sun from inputs: azimuth %.1f deg, altitude %.1f deg"
% (math.degrees(positions[0][0]) % 360.0, math.degrees(positions[0][1])))
else:
report.append("Sun from inputs: %d positions, %d above the horizon" % (len(positions), len(suns)))
if len(suns) == 0:
report.append("No sun position is above the horizon: all fins set perpendicular.")
# --- Which sun rays hit each wall, and how much sun each wall gets ---
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
wall_rays = {}
wall_expo = {}
for name, u, n in WALLS:
rays = []
expo = 0.0
for (sh, w) in suns:
cphi = dot(sh, n)
if cphi > 0.05: # sun is in front of this wall
rays.append((math.atan2(dot(sh, u), cphi), w * cphi))
expo += w * cphi
wall_rays[name] = rays
wall_expo[name] = expo
max_expo = max(wall_expo.values()) if wall_expo else 0.0
total_expo = sum(wall_expo.values())
# --- Best angle per wall ---
target = {} # wall -> (theta_deg, absolute instance rotation in radians)
for name, u, n in WALLS:
rays = wall_rays[name]
share = (100.0 * wall_expo[name] / total_expo) if total_expo > 0 else 0.0
theta = 0.0
if len(rays) == 0:
msg = ("%s wall: gets no direct sun, so fins stay perpendicular (0 deg), "
"which also gives the most diffuse light" % name)
elif wall_expo[name] < MIN_EXPOSURE_FRACTION * max_expo:
msg = ("%s wall: only %.0f%% of the total direct sun, below the "
"MIN_EXPOSURE_FRACTION cutoff, so fins stay at 0 deg" % (name, share))
else:
theta, direct, diffuse, d0, f0 = best_theta(rays, diffuse_dirs)
if abs(theta) < 0.5:
msg = ("%s wall: %.0f%% of the direct sun; perpendicular fins are already the best "
"balance (direct sun reaching the wall %.0f%%, diffuse %.0f%%), so 0 deg"
% (name, share, d0, f0))
else:
msg = ("%s wall: %.0f%% of the direct sun; fins turn %+.0f deg | direct sun reaching the "
"wall %.0f%% (%.0f%% if perpendicular), diffuse %.0f%% (%.0f%%)"
% (name, share, theta, direct, d0, diffuse, f0))
report.append(msg)
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)
# --- 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()