Проблемы с массовой окраской элементов 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ГОТОВО.")

Ответы (0 шт):