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()