IFC Setup Loading fails

Hi, it seems that my Revit Script won’t actually load my IFC Export Options and Property Sets.
I am unable to force Revit to actually load the IFC DLL or something, the exception I got was
”No module named BIM.IFC.Export.UI”

import os
import json
import re
import codecs
import clr
import System

clr.AddReference(“System”)
clr.AddReference(“PresentationFramework”)
clr.AddReference(“RevitAPI”)
clr.AddReference(“RevitAPIIFC”)

from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.IFC import *
from System.Windows import MessageBox, MessageBoxButton, MessageBoxImage
from System.Windows.Markup import XamlReader
from System.IO import FileStream, FileMode
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs
from System.Collections.ObjectModel import ObservableCollection
from System.Collections.Generic import List

doc = revit.ActiveUIDocument.Document

class ExportWindow(object):
...
...
...

# My Issue starts here #

def _get_advanced_ifc_config(self):
    target_name = "RW Export: Nur sichtbares | Pset BT-Liste | Mittel"
    try:
        from BIM.IFC.Export.UI import IFCExportConfigurationsMap, IFCCommandOverrideApplication
        propinfo = clr.GetClrType(IFCCommandOverrideApplication).GetProperty('TheDocument')
        propinfo.SetValue(None, doc)
        configs_map = IFCExportConfigurationsMap()
        configs_map.AddBuiltInConfigurations()
        configs_map.AddSavedConfigurations()
        for config in configs_map.Values:
            if config.Name == target_name: return config
    except: pass
    return None

I made some changes:

pf_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86)
pf_data_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonApplicationData)
ifc_bundle_path = "{0}\\Autodesk\\ApplicationPlugins\\IFC {1}.bundle\\Contents\\{1}".format(pf_data_path, sdk)
ifc_addin_path = "{}\\Autodesk\\Revit {}\\AddIns\\IFCExporterUI".format(pf_path, sdk)

---

    def _get_advanced_ifc_config(self):
        # Initialisierung der Variablen
        configs_map_class = None
        target_name = "RW Export: Nur sichtbares | Pset BT-Liste | Mittel"
        
        try:
            if System.IO.Directory.Exists(ifc_bundle_path):
                sys.path.append(ifc_bundle_path)
                clr.AddReference("IFCExporterUIOverride")
                self._log("ifc_bundle_path geladen")
            elif System.IO.Directory.Exists(ifc_addin_path):
                sys.path.append(ifc_addin_path)
                clr.AddReference("Autodesk.IFC.Export.UI")
                self._log("ifc_addin_path geladen")
    
            # Import der Klassen aus der geladenen DLL
            from BIM.IFC.Export.UI import IFCExportConfigurationsMap, IFCCommandOverrideApplication
            configs_map_class = IFCExportConfigurationsMap
        except Exception as e:
            self._log("Could not load IFC UI Assemblies: {}".format(e))
            return None

        try:
            # WICHTIG: Den Dokument-Kontext für den IFC-Exporter setzen
            if IFCCommandOverrideApplication.TheDocument is None:
                propinfo = clr.GetClrType(IFCCommandOverrideApplication).GetProperty('TheDocument')
                propinfo.SetValue(None, doc)

            # Instanzierung der Map und Laden der Configs
            configs_map = configs_map_class()
            configs_map.AddBuiltInConfigurations()
            configs_map.AddSavedConfigurations()

            # Suche nach der Ziel-Konfiguration
            for config in configs_map.Values:
                if config.Name == target_name:
                    self._log("Konfiguration gefunden: {}".format(target_name))
                    return config
            
            self._log("Konfiguration '{}' NICHT gefunden. Nutze Standard.".format(target_name))
            return None
            
        except Exception as e:
            self._log("Error accessing IFC configurations: {}".format(e))
            return None

----

    def _export_ifc_logic(self, item, view, path, config):
        t = Transaction(doc, "IFC Prep " + item.sID)
        try:
            t.Start()
            
            # --- Ebene & Mapping Logik ---
            levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
            current_lvl = None
            for lvl in levels:
                p_story = lvl.get_Parameter(BuiltInParameter.LEVEL_IS_BUILDING_STORY)
                if p_story:
                    # Namen temporär anpassen für Export (wie in deinem Original)
                    if lvl.Name == item.sRevitName:
                        p_story.Set(1)
                        current_lvl = lvl
                        lvl.Name = item.sID 
                    else: 
                        p_story.Set(0)

            # Fixpunkt-Verschiebung (deine Logik)
            if current_lvl:
                view_els = FilteredElementCollector(doc, view.Id).WhereElementIsNotElementType()
                for el in view_els:
                    if any(x in el.Name.lower() for x in ["pyramide", "nullpunkt", "referenz", "fixpunkt"]):
                        for pid in [BuiltInParameter.FAMILY_LEVEL_PARAM, BuiltInParameter.SCHEDULE_LEVEL_PARAM]:
                            p = el.get_Parameter(pid)
                            if p and not p.IsReadOnly: p.Set(current_lvl.Id)

            # --- IFC EXPORT OPTIONEN ---
            ops = IFCExportOptions()
            
            if config:
                # KRITISCH: Die Konfiguration muss ihre Werte in die 'ops' schreiben!
                # Dies setzt FilterViewId, SpaceBoundaries, SplitWalls, etc.
                config.UpdateOptions(ops, view.Id)
                # Sicherstellen, dass die View-ID wirklich gesetzt ist
                ops.FilterViewId = view.Id
            else:
                # Fallback, falls Config nicht geladen werden konnte
                ops.FilterViewId = view.Id
                ops.AddOption("VisibleElementsInView", "true")

            # Export ausführen
            success = doc.Export(path, item.sIFCName, ops)
            
            if success:
                self._log("  [OK] IFC Export: {}".format(item.sIFCName))
            else:
                self._log("  [!] IFC Export fehlgeschlagen: {}".format(item.sIFCName))

            # WICHTIG: Transaction Rollback, damit Level-Umbenennung nicht gespeichert wird
            t.RollBack()
            
        except Exception as ex:
            if t.HasStarted(): t.RollBack()
            self._log("  [ERR] IFC {}: {}".format(item.sID, str(ex)))

---

self._export_ifc_logic(item, v_ifc, root_path, adv_ifc_config)

now my IFC config is beeing found and IFCs get generated, but the IFC is empfy and the config seems damaged, manual IFC Export doesnt work as well anymore

You only set `FilterViewId` (and options from the config). You never pass an explicit element list to the exporter. So the IFC add-in exports only “what is visible in that view”. If the view shows nothing (view template, phase, discipline, workset, or level filter), the IFC will be empty.

Fix: Build the set of elements you want to export (e.g. from the view) and pass it via options before calling `Export`:

# After building your element list (e.g. from view, same logic as level/fixpunkt)

elements_to_export = list(…)  # your Element set

ELEM_STRING = “;”.join(str(e.Id) for e in elements_to_export)

ops.AddOption(“ElementsForExport”, ELEM_STRING)

# Optional: if you have elements to exclude

# ops.AddOption(“ExcludeFilter”, “;”.join(str(e.Id) for e in exclude_elems))

success = doc.Export(path, item.sIFCName, ops)

Get “elements in view” e.g. with:

view_els = FilteredElementCollector(doc, view.Id).WhereElementIsNotElementType()

# optionally filter: WhereElementIsViewIndependent(), exclude cameras/section boxes, etc.

elements_to_export = list(view_els)

Then add `ElementsForExport` as above. That way the IFC contains that set even when the view’s visibility would show nothing.

2. Why the IFC config seems “damaged” and manual export breaks

You’re setting the static document on the IFC UI entry point:

if IFCCommandOverrideApplication.TheDocument is None:

propinfo = clr.GetClrType(IFCCommandOverrideApplication).GetProperty(‘TheDocument’)

propinfo.SetValue(None, doc)

That affects the global IFC exporter UI state. After your script runs, the add-in may still think “TheDocument” is your document or an invalid state, so the normal IFC export dialog/command can behave wrongly until Revit is restarted.

  • Option A: Restart Revit to clear that static state; then avoid setting `TheDocument` if possible.
  • Option B: Only set it when you really need it for loading configs, and consider resetting it after you’re done (e.g. set back to `None` or to `doc` only for the duration of the config load). That requires checking the add-in’s expected lifecycle (e.g. whether it ever expects `TheDocument` to be null when the user runs Export manually).
  • Option C: Load the config by another path that doesn’t rely on `IFCCommandOverrideApplication.TheDocument` (e.g. file-based or a different API), if the IFC exporter offers one.
1 Like

Thanks, for anyone interested, this seems to work

    def _get_advanced_ifc_config(self):
        configs_map_class = None
        target_name = "RW Export: Nur sichtbares | Pset BT-Liste | Mittel"
        
        # 1. Try to locate and load the DLL safely
        try:
            if os.path.exists(ifc_bundle_path):
                sys.path.append(ifc_bundle_path)
                # Ensure we point to the specific DLL file
                clr.AddReference("IFCExporterUIOverride")
                self._log("Found Bundle: IFCExporterUIOverride loaded")
            elif os.path.exists(ifc_addin_path):
                sys.path.append(ifc_addin_path)
                clr.AddReference("Autodesk.IFC.Export.UI")
                self._log("Found Addin: Autodesk.IFC.Export.UI loaded")
            else:
                self._log("ERR: IFC Export UI DLL not found at expected paths.")
                return None
        except Exception as e:
            self._log("Could not load IFC DLL: {}".format(e))
            return None

        # 2. Use a 'Try' block for the import to catch the specific ImportError
        try:
            from BIM.IFC.Export.UI import IFCExportConfigurationsMap, IFCCommandOverrideApplication
            configs_map_class = IFCExportConfigurationsMap
        except ImportError:
            # Fallback for different Revit/IFC versions where namespace might differ
            try:
                from Autodesk.IFC.Export.UI import IFCExportConfigurationsMap, IFCCommandOverrideApplication
                configs_map_class = IFCExportConfigurationsMap
            except Exception as e:
                self._log("Namespace Import Error: {}".format(e))
                return None

        # 3. Rest of your logic to get the config...
        # (Be sure to include the 'TheDocument' fix and 'finally' block from the previous step)

----------------------------------------------------------------------

# Later...

----------------------------------------------------------------------

    def _export_ifc_logic(self, item, view, path, config):
        t = Transaction(doc, "IFC Prep " + item.sID)
        try:
            t.Start()
            
            # --- Ebene & Mapping Logik ---
            levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
            current_lvl = None
            for lvl in levels:
                p_story = lvl.get_Parameter(BuiltInParameter.LEVEL_IS_BUILDING_STORY)
                if p_story:
                    if lvl.Name == item.sRevitName:
                        p_story.Set(1)
                        current_lvl = lvl
                        lvl.Name = item.sID 
                    else: 
                        p_story.Set(0)

            # Fixpunkt-Verschiebung
            if current_lvl:
                view_els_collector = FilteredElementCollector(doc, view.Id).WhereElementIsNotElementType()
                for el in view_els_collector:
                    if any(x in el.Name.lower() for x in ["pyramide", "nullpunkt", "referenz", "fixpunkt"]):
                        for pid in [BuiltInParameter.FAMILY_LEVEL_PARAM, BuiltInParameter.SCHEDULE_LEVEL_PARAM]:
                            p = el.get_Parameter(pid)
                            if p and not p.IsReadOnly: p.Set(current_lvl.Id)

            # --- ELEMENT SELECTION FIX ---
            # Collect all visible elements in the view to pass explicitly
            visible_els = FilteredElementCollector(doc, view.Id).WhereElementIsNotElementType().ToElementIds()
            ids_string = ";".join([id.ToString() for id in visible_els])

            # --- IFC EXPORT OPTIONEN ---
            ops = IFCExportOptions()
            ops.FilterViewId = view.Id # Define the context
            
            if config:
                config.UpdateOptions(ops, view.Id)
            
            # Force the inclusion of the collected elements
            ops.AddOption("ElementsForExport", ids_string)

            # Export ausführen
            success = doc.Export(path, item.sIFCName, ops)
            
            if success:
                self._log("  [OK] IFC Export: {}".format(item.sIFCName))
            else:
                self._log("  [!] IFC Export failed: {}".format(item.sIFCName))

            t.RollBack() # Revert Level name changes
            
        except Exception as ex:
            if t.HasStarted(): t.RollBack()
            self._log("  [ERR] IFC {}: {}".format(item.sID, str(ex)))

1 Like

I was just finished with the current build, shared it to my collegues aaaaand… it’s broken.

There is a Property set path defined under Property Sets
x Export IFC common property sets
x Export user defined property sets: (Path)

A IFC is created, but it seems to lack the necessary attribute for elements, defined in that list.

I also seem to have forgotten about the correct OPTION B CLEANUP: Reset the static document to None.

Any tips here?

A few things kept getting messed up.

Interesting observation: In the English Version (I work on the german one), it didnt work quiet right alot of times.

I needed to add:

import sys

I also needed to hard code the IFC bundle location

pf_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86)
pf_data_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonApplicationData)
sdk = doc.Application.VersionNumber  # z.B. "2024"

ifc_bundle_path = "{0}\\Autodesk\\ApplicationPlugins\\IFC {1}.bundle\\Contents\\{1}".format(pf_data_path, sdk)
ifc_addin_path = "{}\\Autodesk\\Revit {}\\AddIns\\IFCExporterUI".format(pf_path, sdk)

and modify the two methods.

the whole script.py is underneath. It somehow catches the correct Setup and Property Set List. Be aware, that it could have issues in other Languages

# -*- coding: utf-8 -*-

__title__   = "PROJECT Export Skript - 1.1.3"
__doc__ = """Version: 1.1.3 - Force-Init IFC Service"""

import os
import sys
import json
import re
import codecs
import clr
import System

# Standard Referenzen
clr.AddReference("System")
clr.AddReference("PresentationFramework")
clr.AddReference("RevitAPI")
clr.AddReference("RevitAPIIFC")

# 1. VERSUCH: DLL PFAD ERZWINGEN
from System.Diagnostics import Process
from System.Reflection import Assembly
revit_path = os.path.dirname(Process.GetCurrentProcess().MainModule.FileName)
ifc_ui_dll = os.path.join(revit_path, "AddIns", "IFCExporterUI", "BIM.IFC.Export.UI.dll")

if os.path.exists(ifc_ui_dll):
    try:
        Assembly.LoadFrom(ifc_ui_dll)
        clr.AddReference(ifc_ui_dll)
    except:
        pass

from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.IFC import *
from System.Windows import MessageBox, MessageBoxButton, MessageBoxImage
from System.Windows.Markup import XamlReader
from System.IO import FileStream, FileMode
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs
from System.Collections.ObjectModel import ObservableCollection
from System.Collections.Generic import List

SKRIPT_VERSION = "1.1.3"
PATH_SCRIPT = os.path.dirname(__file__)
CONFIG_PATH = os.path.join(PATH_SCRIPT, "settings.json")
doc = __revit__.ActiveUIDocument.Document

# --- IFC UI PFAD LOGIK (neu) ---
pf_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86)
pf_data_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonApplicationData)
sdk = doc.Application.VersionNumber  # z.B. "2024"

ifc_bundle_path = "{0}\\Autodesk\\ApplicationPlugins\\IFC {1}.bundle\\Contents\\{1}".format(pf_data_path, sdk)
ifc_addin_path = "{}\\Autodesk\\Revit {}\\AddIns\\IFCExporterUI".format(pf_path, sdk)

class ExportWindow(object):
    def __init__(self, xaml_path):
        self.vm = MainViewModel()
        with FileStream(xaml_path, FileMode.Open) as stream: self.ui = XamlReader.Load(stream)
        self.ui.DataContext = self.vm
        self.btn_Start = self.ui.FindName("btn_Start")
        self.listBox_Progress = self.ui.FindName("listBox_Progress")
        self.btn_Start.Click += self.btn_Start_Click
        
        self.config_data = self._load_config_with_check()
        self._setup_items()

    def _load_config_with_check(self):
        if not os.path.exists(CONFIG_PATH): return {}
        try:
            with codecs.open(CONFIG_PATH, "r", encoding="utf-8") as f: 
                data = json.load(f)
                if data.get("version") != SKRIPT_VERSION:
                    res = MessageBox.Show("Skriptversion geändert. Einstellungen übernehmen?", "Version Check", MessageBoxButton.YesNo)
                    if res == MessageBoxButton.No: return {}
                return data
        except: return {}

    def _get_advanced_ifc_config(self):
        # Initialisierung der Variablen
        configs_map_class = None
        target_name = "RW Export: Nur sichtbares | Pset BT-Liste | Mittel"
    
        try:
            if System.IO.Directory.Exists(ifc_bundle_path):
                sys.path.append(ifc_bundle_path)
                clr.AddReference("IFCExporterUIOverride")
                self._log("ifc_bundle_path geladen")
            elif System.IO.Directory.Exists(ifc_addin_path):
                sys.path.append(ifc_addin_path)
                clr.AddReference("Autodesk.IFC.Export.UI")
                self._log("ifc_addin_path geladen")

            # Import der Klassen aus der geladenen DLL
            from BIM.IFC.Export.UI import IFCExportConfigurationsMap, IFCCommandOverrideApplication
            configs_map_class = IFCExportConfigurationsMap

        except Exception as e:
            self._log("Could not load IFC UI Assemblies: {}".format(e))
            return None

        try:
            # Dokument-Kontext für IFC setzen
            if IFCCommandOverrideApplication.TheDocument is None:
                propinfo = clr.GetClrType(IFCCommandOverrideApplication).GetProperty('TheDocument')
                propinfo.SetValue(None, doc)

            # Map instanzieren & Konfigurationen laden
            configs_map = configs_map_class()
            configs_map.AddBuiltInConfigurations()
            configs_map.AddSavedConfigurations()

            # Ziel-Konfiguration suchen
            for config in configs_map.Values:
                if config.Name == target_name:
                    self._log("Konfiguration gefunden: {}".format(target_name))
                    return config
        
            self._log("Konfiguration '{}' NICHT gefunden. Nutze Standard.".format(target_name))
            return None
        
        except Exception as e:
            self._log("Error accessing IFC configurations: {}".format(e))
            return None

    def _get_dwg_options(self):
        try:
            settings = FilteredElementCollector(doc).OfClass(ExportDWGSettings).FirstElement()
            if settings: return settings.GetDWGExportOptions()
        except: pass
        ops = DWGExportOptions()
        ops.MergedViews = True
        ops.ExportLayerOptions = ExportLayerOptions.ExportOnDifferentLayers
        return ops

    def btn_Start_Click(self, sender, e):
        from datetime import datetime
        self.listBox_Progress.Items.Clear()
        self._log(">>> Starte Export (v{})...".format(SKRIPT_VERSION))
        self._save_config()
        
        selected = [i for i in self.vm.Items if i.EbeneChkd]
        if not selected: return

        adv_ifc_config = self._get_advanced_ifc_config()
        dwg_options = self._get_dwg_options()

        # --- ORDNER-LOGIK ---
        has_dwg = any(i.DWGChkd for i in selected)
        has_3d_dwg = any(i.ThreeDDWGChkd for i in selected)
        formate = []
        if has_dwg or has_3d_dwg: formate.append("DWG")
        if any(i.IFCChkd for i in selected): formate.append("IFC")
        if self.vm.PdfManuellChkd: formate.append("PDF")
        
        folder_name = "{} {} {}".format(datetime.now().strftime("%Y-%m-%d"), self._get_geschose_string(selected), ",".join(formate)).strip()
        root_path = os.path.join("Z:\Projekte\PROJECT\Daten\Daten out", folder_name)
        
        dwg_2d_target_path = os.path.join(root_path, self.vm.SubfolderName) if (self.vm.IsSubfolderActive and has_dwg) else root_path

        if not os.path.exists(root_path): os.makedirs(root_path)
        if self.vm.IsSubfolderActive and has_dwg and not os.path.exists(dwg_2d_target_path): os.makedirs(dwg_2d_target_path)

        all_views = [v for v in FilteredElementCollector(doc).OfClass(View).ToElements() if not v.IsTemplate]
        
        for item in selected:
            s_num, s_type = "{:02d}".format(item.num), {"UG":"Untergeschoss","EG":"Erdgeschoss","OG":"Obergeschoss"}.get(item.l_type)
            v_ifc = next((v for v in all_views if s_num in v.Name and s_type in v.Name and "- für IFC Export" in v.Name), None)
            v_dwg = next((v for v in all_views if s_num in v.Name and s_type in v.Name and "- für DWG Export" in v.Name), None)
            v_3d_dwg = next((v for v in all_views if s_num in v.Name and s_type in v.Name and "- für 3D DWG Export" in v.Name), None)

            if item.DWGChkd and v_dwg:
                doc.Export(dwg_2d_target_path, item.sDWGName, List[ElementId]([v_dwg.Id]), dwg_options)

            if item.ThreeDDWGChkd:
                if v_3d_dwg: doc.Export(root_path, item.s3DDWGName, List[ElementId]([v_3d_dwg.Id]), dwg_options)
                else: self._log("  [ERR] 3D View für {} nicht gefunden!".format(item.sID))

            if item.IFCChkd and v_ifc:
                self._export_ifc_logic(item, v_ifc, root_path, adv_ifc_config)

        self._cleanup_pcp(root_path)
        self._log(">>> FERTIG.")
        try:
            if os.path.exists(root_path): os.startfile(root_path)
        except: pass

    def _export_ifc_logic(self, item, view, path, config):
        t = Transaction(doc, "IFC Prep " + item.sID)
        try:
            t.Start()
        
            # --- Ebene & Mapping Logik ---
            levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
            current_lvl = None
            for lvl in levels:
                p_story = lvl.get_Parameter(BuiltInParameter.LEVEL_IS_BUILDING_STORY)
                if p_story:
                    if lvl.Name == item.sRevitName:
                        p_story.Set(1)
                        current_lvl = lvl
                        lvl.Name = item.sID
                    else:
                        p_story.Set(0)

            # Fixpunkt-Verschiebung
            if current_lvl:
                view_els = FilteredElementCollector(doc, view.Id).WhereElementIsNotElementType()
                for el in view_els:
                    if any(x in el.Name.lower() for x in ["pyramide", "nullpunkt", "referenz", "fixpunkt"]):
                        for pid in [BuiltInParameter.FAMILY_LEVEL_PARAM, BuiltInParameter.SCHEDULE_LEVEL_PARAM]:
                            p = el.get_Parameter(pid)
                            if p and not p.IsReadOnly:
                                p.Set(current_lvl.Id)

            # --- IFC EXPORT OPTIONEN ---
            ops = IFCExportOptions()
        
            if config:
                config.UpdateOptions(ops, view.Id)
                ops.FilterViewId = view.Id
            else:
                ops.FilterViewId = view.Id
                ops.AddOption("VisibleElementsInView", "true")

            # Export
            success = doc.Export(path, item.sIFCName, ops)
        
            if success:
                self._log("  [OK] IFC Export: {}".format(item.sIFCName))
            else:
                self._log("  [!] IFC Export fehlgeschlagen: {}".format(item.sIFCName))

            # Rollback (sehr wichtig!)
            t.RollBack()
        
        except Exception as ex:
            if t.HasStarted():
                t.RollBack()
            self._log("  [ERR] IFC {}: {}".format(item.sID, str(ex)))

    def _cleanup_pcp(self, start_path):
        for root, dirs, files in os.walk(start_path):
            for file in files:
                if file.lower().endswith(".pcp"):
                    try: os.remove(os.path.join(root, file))
                    except: pass

    def _log(self, t): self.listBox_Progress.Items.Add(str(t)); self.listBox_Progress.ScrollIntoView(str(t))

    def _get_geschose_string(self, selected_items):
        if not selected_items: return ""
        all_sorted_items = list(self.vm.Items)
        indices = sorted([all_sorted_items.index(i) for i in selected_items])
        def get_name(idx): return all_sorted_items[idx].sRevitName.replace("#", "").strip()
        groups, cur = [], [indices[0]]
        for i in range(1, len(indices)):
            if indices[i] == indices[i-1] + 1: cur.append(indices[i])
            else: groups.append(cur); cur = [indices[i]]
        groups.append(cur)
        parts = []
        for g in groups:
            if len(g) == 1: parts.append(get_name(g[0]))
            elif len(g) == 2: parts.append("{} und {}".format(get_name(g[0]), get_name(g[-1])))
            else: parts.append("{} bis {}".format(get_name(g[0]), get_name(g[-1])))
        return ", ".join(parts)

    def _setup_items(self):
        levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
        temp_list, processed = [], set()
        for lvl in levels:
            fn = lvl.Name.split(" - ")[0].strip()
            n = fn.upper().replace("#", "").strip()
            digits = re.findall(r'\d+', n)
            num = int(digits[0]) if digits else 0
            lt = "UG" if "UG" in n else "OG" if "OG" in n else "EG" if "EG" in n else "?"
            if lt == "?" or (lt == "UG" and 3 <= num <= 5) or (lt == "OG" and num > 13): continue
            if (lt, num) not in processed:
                processed.add((lt, num)); temp_list.append((fn, lt, num))
        
        temp_list.sort(key=lambda x: ({"UG":1,"EG":2,"OG":3}.get(x[1],4), -x[2] if x[1]=="UG" else x[2]))
        for fn, lt, nm in temp_list:
            sid = lt + "{:02d}".format(nm)
            item = EbeneItem(sid, fn, lt, nm)
            item.sDWGName = self.config_data.get(sid+"_DWG", "GR_{}_100_100".format(fn.replace("#","")))
            item.s3DDWGName = self.config_data.get(sid+"_3D", "D_{}_100_100".format(fn.replace("#","")))
            item.sIFCName = self.config_data.get(sid+"_IFC", "3D_{}_100_YYY".format(fn.replace("#","")))
            item.add_PropertyChanged(self._on_item_property_changed)
            self.vm.Items.Add(item)

    def _save_config(self):
        data = {"version": SKRIPT_VERSION}
        for i in self.vm.Items:
            data[i.sID+"_DWG"], data[i.sID+"_3D"], data[i.sID+"_IFC"] = i.sDWGName, i.s3DDWGName, i.sIFCName
        with codecs.open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(data, f, indent=4, ensure_ascii=False)
    
    def _on_item_property_changed(self, s, a): 
        self.btn_Start.IsEnabled = any(i.EbeneChkd for i in self.vm.Items)
        self.vm.IsSubfolderActive = any(i.DWGChkd for i in self.vm.Items)

    def show(self): self.ui.ShowDialog()

class NotifyObject(INotifyPropertyChanged):
    def __init__(self): self._property_changed_handlers = []
    def add_PropertyChanged(self, h): self._property_changed_handlers.append(h)
    def OnPropertyChanged(self, name):
        args = PropertyChangedEventArgs(name); [h(self, args) for h in list(self._property_changed_handlers)]

class EbeneItem(NotifyObject):
    def __init__(self, sID, sRevitName, l_type, num):
        super(EbeneItem, self).__init__()
        self.sID, self.sRevitName, self.l_type, self.num = sID, sRevitName, l_type, num
        self._dwg = self._3d = self._ifc = False
        self.sDWGName = self.s3DDWGName = self.sIFCName = ""
    @property
    def EbeneChkd(self): return self._dwg or self._3d or self._ifc
    @property
    def DWGChkd(self): return self._dwg
    @DWGChkd.setter
    def DWGChkd(self, v): self._dwg = v; self.OnPropertyChanged("DWGChkd"); self.OnPropertyChanged("EbeneChkd")
    @property
    def ThreeDDWGChkd(self): return self._3d
    @ThreeDDWGChkd.setter
    def ThreeDDWGChkd(self, v): self._3d = v; self.OnPropertyChanged("ThreeDDWGChkd"); self.OnPropertyChanged("EbeneChkd")
    @property
    def IFCChkd(self): return self._ifc
    @IFCChkd.setter
    def IFCChkd(self, v): self._ifc = v; self.OnPropertyChanged("IFCChkd"); self.OnPropertyChanged("EbeneChkd")

class MainViewModel(NotifyObject):
    def __init__(self):
        super(MainViewModel, self).__init__()
        self.Items = ObservableCollection[object]()
        self._subName, self._subActive, self._pdfMan = "DWG für Nutzerabstimmung", True, False
    @property
    def SubfolderName(self): return self._subName
    @SubfolderName.setter
    def SubfolderName(self, v): self._subName = v; self.OnPropertyChanged("SubfolderName")
    @property
    def IsSubfolderActive(self): return self._subActive
    @IsSubfolderActive.setter
    def IsSubfolderActive(self, v): self._subActive = v; self.OnPropertyChanged("IsSubfolderActive")
    @property
    def PdfManuellChkd(self): return self._pdfMan
    @PdfManuellChkd.setter
    def PdfManuellChkd(self, v): self._pdfMan = v; self.OnPropertyChanged("PdfManuellChkd")

window = ExportWindow(os.path.join(PATH_SCRIPT, 'MainWindow.xaml'))
window.show()