Python related pyRevit Script for File Export

Hello, I have one really weird bug that I cannot locate at all.
(I hope Im allowed to post this in this section?)

In the pyRevit script I try to export a set list of selected Views, which works amazingly well… right till the point where you have selected a story above Ground Level and as the file type “3D DWG”.
Suddenly everything breaks and no matter how hard I tried, it doesnt want to work at all.

[ERR] 3D Ansicht für OG01 (01 Obergeschoss) nicht gefunden!

The search matrix is the same, the way files are named in the project is the same, I just cannot figure it out at all. I actually lost sleep to it ^^

I now suspect UTF 8, the german letter “ü” or some memory bug due to the length of the stream/parameter is causing the issue, but so far, no luck at all.

The Levels are named like this:

  • #02. Untergeschoss - für … Export

  • #01. Untergeschoss - für … Export

  • #00. Erdgeschoss - für … Export

  • 01. Obergeschoss - für … Export

The Code - with german comments and parameters :]
I susopect the bug is hidden in line 120-135 or something like that

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

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

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):
        target_name = "XY 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

    def _get_dwg_options(self):
        # Versucht, die im Projekt definierten DWG-Einstellungen zu laden (Layer-Mapping etc.)
        try:
            settings = FilteredElementCollector(doc).OfClass(ExportDWGSettings).FirstElement()
            if settings:
                return settings.GetDWGExportOptions()
        except: pass
        
        # Fallback, falls keine Einstellungen im Projekt sind
        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()
        # Hole die korrekten DWG Settings aus dem Projekt für Layerzuordnung
        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(os.path.expanduser("~"), "Desktop", "DatenOut", folder_name)
        
        # Bestimme den Zielpfad für 2D DWGs basierend auf der Checkbox
        if self.vm.IsSubfolderActive and has_dwg:
            dwg_2d_target_path = os.path.join(root_path, self.vm.SubfolderName)
        else:
            dwg_2d_target_path = root_path

        # Ordner erstellen
        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)
            
            # Ansichtensuche für alle Formate
            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)

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

            # I suspect the Bug to be here
            # ----------------------------
            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 Ansicht für {} ({} {}) nicht gefunden!".format(item.sID, s_num, s_type))

            # IFC Export
            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()
            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)

            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)

            ops = IFCExportOptions()
            if config: config.UpdateOptions(ops, view.Id)
            else: ops.FilterViewId = view.Id
            doc.Export(path, item.sIFCName, ops)
            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 ""
        # Wir nutzen die Liste der Items aus dem ViewModel, da diese bereits korrekt sortiert ist
        all_sorted_items = list(self.vm.Items)
        # Indizes der gewählten Items in der Master-Liste finden
        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 = []
        if indices:
            cur = [indices[0]]
            for i in range(1, len(indices)):
                # Wenn der Index fortlaufend ist, gehört er zur selben Gruppe
                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", "000_ABC_GR_{}_100_100_DEFG_01_00".format(fn.replace("#","")))
            item.s3DDWGName = self.config_data.get(sid+"_3D", "000_ABC_3D_{}_100_100_DEFG_01_00".format(fn.replace("#","")))
            item.sIFCName = self.config_data.get(sid+"_IFC", "000_ABC_3D_{}_100_XXX_DEFG_01".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()

Have you tried renaming it without the characters and shorter length?

I did and in the confusion the shorter parameters made more chaos, causing the script to not run anymore at all. Im afraid to change much, as everything finally works, well almost.

No, I’m hesitant to change projekt Names and Views, it’s in a Live Project.

It also makes not really sense to me, since the longer #02. Untergeschoss shouldnt work either then.

I can try if yall think that is the issue, but I wasted so much time on this, I was hoping somebody spots a missing , or . and Im done

The error simply says the view isn’t available, are you sure that view exists in your project?

1 Like

My dumba55 wrote “Oberrgeschoss” instead of “Obergeschoss”.

Error in OSI Layer 8 # I’ the problem lol