Проблемы с массовой окраской элементов IFC2X3 по GUID в Trimble Desktop
Пытаюсь автоматически покрасить все элементы IFC-модели по GUID с помощью Python и IfcOpenShell.
Исходная модель — IFC2X3, ~16 000 элементов.
Есть Excel с GUID и HEX/RGB цветами.
A B GUID color 3M6Xx_c_D1BOdyE9KdDRD_ #00BFFF
Использую скрипт, который создаёт IfcMaterial и связывает его с каждым элементом через IfcRelAssociatesMaterial.
Проблема:
Скрипт показывает, что все элементы «закрашены», но в Trimble Desktop реально цвет виден только на части элементов (~1000).
Некоторые элементы с геометрией остаются без цвета.
Элементы без Representation вообще не окрашиваются.
Попытки использовать StyledItem или PresentationLayerAssignment приводят к ошибкам или к тому, что модель не открывается.
Вопрос:
Как корректно и гарантированно визуально окрашивать все элементы IFC по GUID для просмотра в Trimble Desktop?
Нужно ли переводить модель в IFC4, и как это лучше сделать, чтобы сохранить совместимость и все GUID?
Есть ли рекомендации по обработке больших моделей (>10 000 элементов) для массовой окраски?
Ниже представлен код на python который закрашивает часть элементов:
# ifc_colorizer_v2.py
import ifcopenshell
import pandas as pd
import os
import uuid
import traceback
# ============ НАСТРОЙКИ ============
# Скрипт автоматически подхватит первый .ifc и первый .xlsx/.xls в текущей папке
OUT_IFC = "model_colored.ifc"
LOG_FILE = "color_log.txt"
def find_first(exts):
for f in os.listdir('.'):
if any(f.lower().endswith(e) for e in exts):
return f
return None
IFC_FILE = find_first(['.ifc'])
EXCEL_FILE = find_first(['.xlsx', '.xls'])
if not IFC_FILE:
print("❌ IFC не найден в папке.")
raise SystemExit
if not EXCEL_FILE:
print("❌ Excel не найден в папке.")
raise SystemExit
print("IFC:", IFC_FILE)
print("Excel:", EXCEL_FILE)
# ============ ЗАГРУЗКА ============
ifc = ifcopenshell.open(IFC_FILE)
df = pd.read_excel(EXCEL_FILE)
if "GUID" not in df.columns or "color" not in df.columns:
print("❌ В Excel должно быть две колонки: GUID и color")
raise SystemExit
# Парсер цвета: #RRGGBB или "R,G,B" (0-255)
def parse_color(s):
s = str(s).strip()
if s.startswith("#") and len(s) >= 7:
s2 = s.lstrip("#")[:6]
r = int(s2[0:2], 16) / 255.0
g = int(s2[2:4], 16) / 255.0
b = int(s2[4:6], 16) / 255.0
return (r, g, b)
if "," in s:
parts = [p.strip() for p in s.split(",")]
if len(parts) == 3:
# assume 0-255 ints
return (float(parts[0]) / 255.0, float(parts[1]) / 255.0, float(parts[2]) / 255.0)
raise ValueError("Неизвестный формат цвета: " + s)
# Формируем словарь guid->rgb
guid_color = {}
for _, row in df.iterrows():
guid = str(row["GUID"]).strip().replace("{", "").replace("}", "")
try:
rgb = parse_color(row["color"])
guid_color[guid] = rgb
except Exception as e:
print(f"⚠ Пропущен {guid}: некорректный цвет -> {row['color']}")
print("Цветов для обработки:", len(guid_color))
# ============ ФУНКЦИИ СОЗДАНИЯ/КЭША ============
style_cache = {} # rgb tuple -> IfcPresentationStyleAssignment (or IfcSurfaceStyle for IFC4)
def build_minimal_style(ifc, rgb):
"""Создать и вернуть IfcPresentationStyleAssignment, содержащий IfcSurfaceStyle с IfcSurfaceStyleRendering и IfcColourRgb"""
# rgb: tuple (r,g,b), 0..1
r, g, b = rgb
# создаём IfcColourRgb
colour = ifc.createIfcColourRgb(None, r, g, b)
# For IFC2x3: create IfcSurfaceStyleRendering with minimal args (SurfaceColour + others None)
try:
rendering = ifc.createIfcSurfaceStyleRendering(colour, None, None, None, None, None, None, None)
except TypeError:
# Some builds accept fewer/more args — try minimal positional
rendering = ifc.createIfcSurfaceStyleRendering(colour, None, None, None, None, None, None)
surf = ifc.createIfcSurfaceStyle(None, "BOTH", [rendering])
pres = ifc.createIfcPresentationStyleAssignment([surf])
return pres
def get_style(ifc, rgb):
key = (round(rgb[0],6), round(rgb[1],6), round(rgb[2],6))
if key in style_cache:
return style_cache[key]
style = build_minimal_style(ifc, rgb)
style_cache[key] = style
return style
# ============ ГЛАВНЫЙ ЦИКЛ ============
ok = []
not_found = []
no_geom = []
errors = []
mapped_handled = []
print("Начинаю окраску...")
for guid, rgb in guid_color.items():
try:
element = ifc.by_guid(guid)
if not element:
not_found.append(guid)
continue
# Собираем items: прямые Items и mapped
items_to_style = []
rep = getattr(element, "Representation", None)
if rep:
for representation in rep.Representations:
if not hasattr(representation, "Items"):
continue
for itm in representation.Items:
items_to_style.append(itm)
# Если встретили IfcMappedItem — добавим также его MappingRepresentation items
try:
if itm.is_a("IfcMappedItem"):
src = getattr(itm, "MappingSource", None)
if src and hasattr(src, "MappingRepresentation"):
mr = src.MappingRepresentation
for rep2 in mr.Representations:
if hasattr(rep2, "Items"):
for itm2 in rep2.Items:
items_to_style.append(itm2)
mapped_handled.append(guid)
except Exception:
pass
# Если не нашли items — возможно, у продукта нет геометрии
if not items_to_style:
no_geom.append(guid)
continue
style = get_style(ifc, rgb)
# Применяем стиль к каждому item. Удаляем старые StyledByItem, затем создаём IfcStyledItem.
for item in items_to_style:
# безопасно удалить старые стили (если есть)
try:
old = getattr(item, "StyledByItem", None)
if old:
# копируем чтобы не изменять список во время итерации
for s in list(old):
try:
ifc.remove(s)
except Exception:
# некоторые реализации не позволяют remove — игнорируем
pass
except Exception:
pass
# Создаём IfcStyledItem — в IFC2X3 часто ожидается IfcPresentationStyleAssignment в Styles.
try:
ifc.createIfcStyledItem(Item=item, Styles=[style], Name=None)
except Exception as e:
# fallback: некоторые сборки требуют IfcSurfaceStyle напрямую (unlikely) — попробуем добавить surf from pres
try:
# если style - IfcPresentationStyleAssignment, возьмём первый surface style
if hasattr(style, "Styles") and len(style.Styles) > 0:
surf = style.Styles[0]
ifc.createIfcStyledItem(Item=item, Styles=[surf], Name=None)
except Exception as e2:
errors.append((guid, str(e), str(e2)))
# продолжаем, не ломая весь процесс
ok.append(guid)
except Exception as e:
errors.append((guid, str(e)))
# не прерываем цикл
# ============ ЛОГ ============
with open(LOG_FILE, "w", encoding="utf-8") as f:
f.write("IFC Colorizer v2 log\n")
f.write("Input IFC: " + IFC_FILE + "\n")
f.write("Input Excel: " + EXCEL_FILE + "\n\n")
f.write("Закрашено GUID (count={}):\n".format(len(ok)))
for g in ok: f.write(" OK: " + g + "\n")
f.write("\nНе найдено GUID (count={}):\n".format(len(not_found)))
for g in not_found: f.write(" NOT_FOUND: " + g + "\n")
f.write("\nGUID без геометрии (count={}):\n".format(len(no_geom)))
for g in no_geom: f.write(" NO_GEOM: " + g + "\n")
f.write("\nGUID mapped (включены mapping items) (count={}):\n".format(len(mapped_handled)))
for g in set(mapped_handled): f.write(" MAPPED: " + g + "\n")
f.write("\nОшибки (count={}):\n".format(len(errors)))
for e in errors: f.write(" ERR: " + repr(e) + "\n")
print("\nРезультат:")
print(" Закрашено (GUID):", len(ok))
print(" Не найдено:", len(not_found))
print(" Без геометрии:", len(no_geom))
print(" Mapped GUID (обработаны):", len(set(mapped_handled)))
print(" Ошибок:", len(errors))
# ============ Сохранение ============
try:
if os.path.exists(OUT_IFC):
os.remove(OUT_IFC)
ifc.write(OUT_IFC)
print("\nСохранено в:", OUT_IFC)
print("Лог сохранён в:", LOG_FILE)
except Exception as e:
print("❌ Ошибка при сохранении:", e)
print("Попробуй сохранить под другим именем или проверить права записи.")
print("\nГОТОВО.")