Dynamo и числа: AsDouble, футы и почему размеры не сходятся
AsDouble возвращает футы, AsValueString — строку по настройкам проекта. Разбираем внутренние единицы Revit, UnitTypeId, округление и выгрузку размеров в мм.
В модели дверь шириной 3000. Скрипт выводит 9.842519685039369. Ошибки нет, всё честно: Revit хранит длины в футах, а 3000 / 304.8 как раз и есть 9.8425.
На этом месте застревает каждый, кто впервые открыл узел Python Script и дёрнул AsDouble(). Дальше начинается интереснее: округление, накопление погрешности, запятая вместо точки в русской локали и спецификация, где объём отличается от правильного в 35 раз. Разберём по порядку, с кодом.
Что Revit хранит внутри
Отображение на экране и хранение в базе — разные вещи. Настройки единиц проекта (вкладка «Управление» → «Единицы проекта») меняют только показ. Внутри всегда одно и то же:
- длины — в десятичных футах;
- площади — в квадратных футах;
- объёмы — в кубических футах;
- углы — в радианах;
- координаты точек — в футах от внутреннего начала координат.
Коэффициенты, которые стоит держать в голове: 304.8 мм в футе, 0.09290304 м² в квадратном футе, 0.028316846 м³ в кубическом.
Отсюда классика жанра. Забыли пересчитать объём — цифры в ведомости бетона отличаются в 35 раз. Умножили длину на 304.8 дважды — получили километры вместо метров. И самое обидное: записали в параметр 3000, имея в виду миллиметры, а Revit прочитал 3000 футов и построил элемент длиной 914 метров.
AsDouble, AsValueString, AsString: чем отличаются
Три метода параметра, которые путают чаще всего.
AsDouble() возвращает число во внутренних единицах. Для длины — футы. Работает только если у параметра StorageType = Double, иначе вернёт мусор или ноль.
AsValueString() возвращает строку ровно в том виде, в каком значение показано в интерфейсе: «3000» или «3000,0 мм», в зависимости от настроек единиц проекта. Удобно для отчёта, опасно для вычислений.
AsString() для числового параметра вернёт None. Он для текстовых. Отсюда любимая ошибка новичка: AttributeError: 'NoneType' object has no attribute 'strip'.
Правильный порядок — сначала спросить тип хранения, потом читать:
from Autodesk.Revit.DB import StorageType
def prochitat(p):
if p is None or not p.HasValue:
return None
st = p.StorageType
if st == StorageType.Double:
return p.AsDouble() # футы, радианы, кубофуты
if st == StorageType.Integer:
return p.AsInteger() # в том числе Да/Нет: 1 и 0
if st == StorageType.String:
return p.AsString()
if st == StorageType.ElementId:
return p.AsElementId()
return None
Пять строк проверки экономят вечер отладки на разнородной выборке, где у одного семейства параметр есть, а у другого нет.
Конвертация: UnitUtils и UnitTypeId
С Revit 2021 единицы описываются через ForgeTypeId, и рабочая пара выглядит так:
from Autodesk.Revit.DB import UnitUtils, UnitTypeId
futy = param.AsDouble()
mm = UnitUtils.ConvertFromInternalUnits(futy, UnitTypeId.Millimeters)
# обратно, когда пишем в модель
param.Set(UnitUtils.ConvertToInternalUnits(3000.0, UnitTypeId.Millimeters))
Полезные значения: UnitTypeId.Millimeters, Meters, SquareMeters, CubicMeters, Degrees, Kilograms.
Старый код на DisplayUnitType.DUT_MILLIMETERS писался под Revit 2020 и раньше. В 2021 он помечен устаревшим, в 2022 из API убран, и скрипт падает с AttributeError: 'DisplayUnitType' object has no attribute ... или сообщением о неподдерживаемом типе. Если тащите библиотеку скриптов из старых версий, это первое, что придётся переписать.
Почему не просто делить на 304.8? Для длин разницы нет. Для площадей, объёмов и всего, что связано с массой и температурой, коэффициенты нетривиальны, и UnitUtils избавляет от их поиска. Плюс код читается: ConvertFromInternalUnits(v, UnitTypeId.CubicMeters) понятнее, чем v * 0.028316846.
Отдельная деталь — общие параметры типа «Число» (Number). У них нет единиц вообще, значение хранится как есть. Конвертировать их нельзя, иначе коэффициент армирования 0.85 превратится в 259.
Почему в ноде 3000, а в питоне 9.84
Дело в том, что штатные узлы Dynamo уже конвертируют значения. Element.GetParameterValueByName отдаёт число в единицах проекта, а вызов API внутри Python Script — во внутренних. Один и тот же параметр, два разных числа, и оба правильные.
Практический вывод: не смешивайте источники в одном графе. Либо всё читается узлами и дальше идёт как есть, либо всё читается через API и конвертируется на выходе. Гибрид даёт ошибку в 304.8 раза, которую замечают уже в напечатанной ведомости.
То же касается геометрии. Координаты точек, полученных из API, живут в футах, и складывать их с координатами из Dynamo-узлов без пересчёта нельзя.
Как устроен сам граф и куда вставлять питон, разбирал раньше: первый скрипт в Dynamo.
Округление и накопление ошибки
Правило одно: считать в футах, округлять только на выводе.
Соблазн округлить сразу понятен — числа страшные. Цена ошибки видна на простом примере. Берём 500 участков воздуховода, каждый по 1.2345 фута:
dliny = [1.2345] * 500
# плохо: округлили каждый кусок в мм, потом сложили
a = sum(round(UnitUtils.ConvertFromInternalUnits(d, UnitTypeId.Millimeters))
for d in dliny)
# правильно: сложили в футах, конвертировали один раз, округлили в конце
b = round(UnitUtils.ConvertFromInternalUnits(sum(dliny), UnitTypeId.Millimeters), 1)
Разница на пятистах элементах набегает в несколько сантиметров, на тысячах — в десятки. В ведомости длин это выглядит как расхождение с моделью, которое невозможно объяснить заказчику.
Второе следствие — сравнение. Два числа с плавающей точкой никогда не сравнивают знаком равенства:
# так нельзя
if dlina_a == dlina_b:
...
# так можно: допуск 0,5 мм, переведённый в футы
DOPUSK = 0.5 / 304.8
if abs(dlina_a - dlina_b) < DOPUSK:
...
У самого Revit есть встроенный порог: doc.Application.ShortCurveTolerance — примерно 1/256 фута, около 0.8 мм. Короче этого линию он просто не создаст, и попытка кончится исключением.
Локаль: запятая, которая ломает всё
Самый неочевидный баг в русском Revit. AsValueString() на длине вернёт что-то вроде 3 000,00, где разделитель дробной части — запятая, а разделитель тысяч — неразрывный пробел (\xa0).
s = param.AsValueString() # '3 000,00'
float(s) # ValueError: could not convert string to float
Чинится в одну строку, но знать про это надо заранее:
def v_chislo(s):
if not s:
return None
s = s.replace('\xa0', '').replace(' ', '').replace(',', '.')
s = ''.join(c for c in s if c.isdigit() or c in '.-')
return float(s) if s else None
Отсюда общее правило обмена: AsValueString годится для того, что человек прочитает глазами. Для расчётов, выгрузок и сверок берётся AsDouble с явной конвертацией. Настройки единиц у соседнего файла могут отличаться, и тогда одна и та же выгрузка в понедельник даст миллиметры, а в среду сантиметры.
Обратная операция — SetValueString("3000") — разбирает строку по настройкам проекта. Удобно, когда значение пришло от пользователя из диалога, а не из расчёта.
Выгрузка в спецификацию
Собирая данные для Excel, конвертируйте на последнем шаге и сразу задавайте точность вывода.
stroki = []
for e in UnwrapElement(IN[0]):
p_dl = e.LookupParameter("Длина")
p_ob = e.LookupParameter("Объем")
dlina_mm = UnitUtils.ConvertFromInternalUnits(p_dl.AsDouble(), UnitTypeId.Millimeters) if p_dl else 0
obem_m3 = UnitUtils.ConvertFromInternalUnits(p_ob.AsDouble(), UnitTypeId.CubicMeters) if p_ob else 0
stroki.append([
e.Id.IntegerValue,
round(dlina_mm, 1), # мм с одним знаком
round(obem_m3, 3), # м³ с тремя
])
OUT = stroki
Точность в 0.1 мм и 0.001 м³ покрывает практически любую форму. Больше знаков — таблица становится нечитаемой, меньше — итоги перестают сходиться с моделью.
Как довести такую выгрузку до готовой формы заказчика, расписано в статье про спецификации Revit в Excel.
Проверка себя за минуту
Прежде чем гнать скрипт по всей модели, возьмите один элемент с известным размером. Стену длиной ровно 6000 мм, круглый воздуховод диаметром 200, плиту толщиной 200. Выведите по нему все три величины рядом: сырое значение из AsDouble, результат конвертации и строку из AsValueString.
Дальше сверяете глазами. Сырое 19.685 и конвертированное 6000 при показанных «6000» — цепочка собрана правильно. Конвертированное 19.685 означает, что конвертацию где-то потеряли по дороге. Конвертированное 1828800 — что применили её дважды.
Тридцать секунд на такой контрольный элемент перед каждой новой выгрузкой стоят дешевле, чем разбирательство с ведомостью, которую уже отправили заказчику. У нас это вписано в шаблон графа отдельным узлом Watch, который никогда не удаляется: слева контрольный элемент, справа три числа.
Второй способ контроля — сверка итога со спецификацией самого Revit. Соберите ту же величину штатной спецификацией с включённой строкой итогов и сравните с выводом скрипта. Расхождение в третьем знаке нормально, расхождение в первом означает либо единицы, либо разный состав выборки: спецификация могла отфильтровать элементы, которых ваш скрипт не отсеял.
Где это не поможет
Конвертация не чинит неправильно заданный тип параметра. Если общий параметр создан как «Текст», а туда записывают размеры, никакой UnitUtils не спасёт: там строка, и в ней рано или поздно окажется «3000 (уточнить)».
Не поможет и там, где параметр вычисляемый. Формула в семействе считается по своим правилам, и запись в результат просто не пройдёт: параметр будет помечен как read-only, а Set() выбросит исключение о попытке изменить неизменяемое значение.
Углы отдельная песня. Радианы конвертируются штатно, но поворот 90° внутри семейства и поворот экземпляра в проекте — разные величины, и путать их UnitUtils не мешает.
И последнее: единицы не гарантируют правильность данных. Скрипт честно переведёт в миллиметры высоту установки розетки, которую инженер вбил наугад. Проверка смысла — отдельная работа, и её удобно ставить рядом с автоматизацией: автоматизация проверки моделей.
Короткие ответы
Почему AsDouble возвращает 9.84 вместо 3000? Потому что внутренние единицы Revit — футы. 3000 мм / 304.8 = 9.8425. Конвертируйте через UnitUtils.ConvertFromInternalUnits.
Чем AsValueString отличается от AsDouble? Первый отдаёт строку в единицах проекта и годится для показа человеку, второй — число во внутренних единицах и годится для расчётов.
Как узнать, в каких единицах показан параметр? doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId() вернёт текущую настройку длины для документа.
Работает ли старый код с DisplayUnitType? В Revit 2020 и раньше да, начиная с 2022 нет. Заменяется на UnitTypeId без изменения остальной логики.
Почему сумма длин не совпадает со спецификацией Revit? Чаще всего из-за раннего округления в скрипте. Складывайте во внутренних единицах, округляйте один раз на выводе.
Что сделать прямо сейчас
Откройте любой свой рабочий граф и найдите в нём число 304.8 или 3.28. Если оно встречается больше одного раза, там уже сидит будущая ошибка — вынесите конвертацию в одну функцию на выходе.
Нужен скрипт, который читает модель и отдаёт цифры в правильных единицах и в вашей форме, — напишите в Telegram или через контакты. Разбор модели и оценка занимают день.
Нужен похожий BIM/AI процесс?
Напишите в Telegram или на email — разберём задачу и предложим архитектуру решения.
Telegram Оставить заявку