From b9462e61c5c9ba9e9684f9ce276c3744e6f43767 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 27 Sep 2026 10:51:13 +0200 Subject: [PATCH] Defer Revit work and remove Dynamo scripts Further Revit exchange will use the CSV export/import of the Linear plugin instead of Dynamo, as preferred by the internal Revit/CAD steering committee. Mark the Revit documentation as deferred and remove the Dynamo diagnostic scripts, which stay reachable through the tag archive/dynamo-vorueberlegungen. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 - AGENTS.md | 15 +- README.md | 4 +- docs/README.md | 3 +- docs/circuit-list-editor-known-limitations.md | 2 + docs/current-architecture.md | 4 + ...history-and-external-model-architecture.md | 4 + docs/revit-requirements-llm-context.md | 4 + docs/spec/07-implementation-phases-todo.md | 6 +- docs/spec/08-current-product-backlog.md | 3 +- .../revit-csv-integration-requirements.md | 15 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 4 + dynamo/01_check_model_identity.py | 385 ------------- ..._electrical_fixture_parameter_inventory.py | 517 ------------------ dynamo/README.md | 69 --- 15 files changed, 58 insertions(+), 978 deletions(-) delete mode 100644 dynamo/01_check_model_identity.py delete mode 100644 dynamo/02_export_electrical_fixture_parameter_inventory.py delete mode 100644 dynamo/README.md diff --git a/.gitignore b/.gitignore index e0ef1d2..a6e1ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ dist/ data/*.db data/backups/*.db .codex/*.log -dynamo/output/ diff --git a/AGENTS.md b/AGENTS.md index 704aee9..4298762 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -532,6 +532,18 @@ Required operations: ## Revit / CSV External Model Work +Status: further Revit work (editor assignment UI, follow-up import, return +export) is deferred and will be tackled later. When it resumes, the exchange +stays CSV-based and uses the CSV export/import of the Linear plugin in Revit. +The internal Revit/CAD steering committee prefers this over manipulating the +model directly through Dynamo. Do not start Dynamo, Revit API or JSON +transport work. The Linear CSV layout is not yet known: check a sample export +against the existing project CSV configuration before changing the parser. +Earlier Dynamo considerations are archived in +`docs/archive/revit-dynamo-leistungsbilanz-integration-konzept.md`; the +removed diagnostic scripts remain reachable through the git tag +`archive/dynamo-vorueberlegungen`. + When explicitly working on Phase 14, use `docs/spec/revit-csv-integration-requirements.md` as the detailed source of truth together with the current architecture documents. @@ -620,7 +632,8 @@ Users must be able to override sizing suggestions. ## Current Deferred Work -- Revit/CSV/IFCGUID round-trip, except when Phase 14 is explicitly requested and +- Revit/CSV/IFCGUID round-trip (deferred; later via Linear plugin CSV, never + Dynamo), except when Phase 14 is explicitly requested and `docs/spec/revit-csv-integration-requirements.md` is being followed - full electrical sizing - multi-user/PostgreSQL operation diff --git a/README.md b/README.md index 2577989..b3c026e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ Docker-Server-Stack mit gebautem Anwendungscode und ein separater Entwicklungsstack bereit. Ein vollständig unterstütztes Produktionsdeployment und Mehrbenutzerbetrieb fehlen weiterhin. Revit-CSV-Konfiguration, Vorschau und bestätigter Erstimport sind implementiert; die Editoroberfläche zur -Objektzuordnung, Folgeimporte und Rückexport stehen noch aus. +Objektzuordnung, Folgeimporte und Rückexport stehen noch aus. Die weitere +Revit-Arbeit ist zurückgestellt und erfolgt später über den CSV-Export/-Import +des Linear-Plugins in Revit. ## Unterstützter Arbeitsablauf diff --git a/docs/README.md b/docs/README.md index c7a3d88..4f84717 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,7 +29,8 @@ ausdrücklich getrennt und dürfen nicht als bereits implementiert verstanden we - [Projektversionen und externer Modellaustausch](project-history-and-external-model-architecture.md) - [LLM-Kontext für die Revit-Anforderungsplanung](revit-requirements-llm-context.md) -- [Revit-CSV-Anforderungen](spec/revit-csv-integration-requirements.md) +- [Revit-CSV-Anforderungen](spec/revit-csv-integration-requirements.md) – + zurückgestellt; später über den CSV-Export/-Import des Linear-Plugins - [Phase-14-Audit und Implementierungsplan](spec/revit-csv-phase-14-audit-and-plan.md) - [Zukünftige Dimensionierung](spec/06-future-sizing-and-calculations.md) - [Roadmap und Phasen](spec/07-implementation-phases-todo.md) diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 3e70e76..a3840df 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -9,6 +9,8 @@ - Named and automatic logical project snapshots can be listed and restored through the project page with persistent Undo/Redo. Automatic snapshots are created every 25 revisions and only their newest 12 entries are retained. +- Further Revit work is deferred; it will later use the CSV export/import of + the Linear plugin in Revit, not Dynamo. - The Revit/CSV foundation and confirmed initial import are implemented. Existing-row assignment, move and detach plus creation of an external-only row in an existing or newly created circuit have atomic persistent commands. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 1840d59..6eeb6e1 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -552,6 +552,10 @@ bei einem späteren Wechsel trotzdem einen eigenen PostgreSQL-Adapter. ## Revit-CSV-Grundlage +**Zurückgestellt:** Die weitere Revit-Arbeit wird später angegangen, dann +über den CSV-Export/-Import des Linear-Plugins in Revit statt über Dynamo. +Details: [Revit-CSV-Anforderungen, Abschnitt 1](spec/revit-csv-integration-requirements.md). + Der reine Adapter unter `src/external-model/csv` validiert die projektweite Transport- und Mappingkonfiguration und parst UTF-8-CSV verlustarm in eine klassifizierte Zellmatrix. Er erkennt Metadaten, Header, Passthrough-, Objekt- diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index a7c1300..f8ed263 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -113,6 +113,10 @@ Implemented automatic-snapshot policy: ## Revit / CSV / IFCGUID Round-Trip Direction +**Deferred:** further Revit work will be tackled later, then through the +CSV export/import of the Linear plugin in Revit instead of Dynamo. Details: +[Revit-CSV requirements, section 1](spec/revit-csv-integration-requirements.md). + External model exchange should use explicit staging and link entities instead of writing imported rows directly into circuits. Conceptual entities: diff --git a/docs/revit-requirements-llm-context.md b/docs/revit-requirements-llm-context.md index 314820b..cf10324 100644 --- a/docs/revit-requirements-llm-context.md +++ b/docs/revit-requirements-llm-context.md @@ -1,5 +1,9 @@ # Kontext für die Revit-Anforderungsplanung +**Zurückgestellt:** Die weitere Revit-Arbeit wird später angegangen, dann +über den CSV-Export/-Import des Linear-Plugins in Revit statt über Dynamo. +Details: [Revit-CSV-Anforderungen, Abschnitt 1](spec/revit-csv-integration-requirements.md). + ## Zweck dieser Datei Diese Datei ist eine kompakte Übergabe an ein anderes LLM. Sie soll dabei diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index b665e85..f36d60d 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -532,6 +532,10 @@ Acceptance criteria: ## Phase 14: External Model Round-Trip +**Zurückgestellt:** Die weitere Revit-Arbeit wird später angegangen, dann +über den CSV-Export/-Import des Linear-Plugins in Revit statt über Dynamo. +Details: [Revit-CSV-Anforderungen, Abschnitt 1](revit-csv-integration-requirements.md). + Status: Phase 14.2 (persistente externe Objekte und bestätigter Erstimport) ist implementiert. Editor-Zuordnung, Rückexport und Folgeimport folgen. Siehe [Phase-14-Audit und Implementierungsplan](revit-csv-phase-14-audit-and-plan.md) @@ -540,7 +544,7 @@ und die verbindlichen Goal: -Exchange model objects and planning parameters with Revit or comparable tools through CSV or another agreed format. +Exchange model objects and planning parameters with Revit through CSV (Linear plugin export/import). Tasks: diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index c90dd24..a9e902d 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -141,4 +141,5 @@ The pre-release cleanup established the following compatibility boundary: The Revit/CSV/IFCGUID round-trip follows the distribution-board component foundation as a separate jointly planned phase and must not be inferred from -these tasks. +these tasks. It is currently deferred and will later use the CSV +export/import of the Linear plugin in Revit, not Dynamo. diff --git a/docs/spec/revit-csv-integration-requirements.md b/docs/spec/revit-csv-integration-requirements.md index 973c1a6..4ce65ee 100644 --- a/docs/spec/revit-csv-integration-requirements.md +++ b/docs/spec/revit-csv-integration-requirements.md @@ -2,6 +2,19 @@ ## 1. Status und Verbindlichkeit +> **Zurückgestellt (September 2026).** Die weitere Revit-Arbeit +> (Editor-Oberfläche zur Objektzuordnung, Folgeimport, Rückexport) wird später +> angegangen. Wenn sie wieder aufgenommen wird, bleibt der Austausch +> CSV-basiert: Export und Import laufen über das Linear-Plugin in Revit. Das +> entspricht der Vorgabe des internen Revit/CAD-Lenkungskreises, weil dabei +> nicht direkt per Dynamo im Modell gearbeitet wird. Eine Dynamo- oder +> API-Anbindung ist nicht vorgesehen; die früheren Vorüberlegungen liegen unter +> [docs/archive](../archive/revit-dynamo-leistungsbilanz-integration-konzept.md). +> Der Aufbau der Linear-CSV ist noch offen. Vor der Wiederaufnahme wird ein +> Musterexport gegen die bestehende projektweite CSV-Konfiguration und den +> Parser geprüft; erst danach wird entschieden, was anzupassen ist. Die bereits +> umgesetzten Schritte bleiben unverändert nutzbar. + Dieses Dokument ist die verbindliche fachliche und technische Arbeitsgrundlage für **Phase 14: External Model Round-Trip**. Es unterscheidet ausdrücklich zwischen: @@ -1338,7 +1351,7 @@ Diese Punkte blockieren die Erstellung dieser Spezifikation nicht. - automatische raumbezogene Stromkreisvorschläge; - projektübergreifend wiederverwendbare Mappingprofile; - Teilmodelle und mehrere externe Quellen; -- Dynamo/API/JSON-Transportadapter; +- Dynamo/API/JSON-Transportadapter (nicht vorgesehen, siehe Abschnitt 1); - Rollen und Berechtigungen; - Hintergrundjobs; - PostgreSQL. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index be50eb1..a4c21e4 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -2,6 +2,10 @@ ## Status +**Zurückgestellt:** Die weitere Revit-Arbeit wird später angegangen, dann +über den CSV-Export/-Import des Linear-Plugins in Revit statt über Dynamo. +Details: [Revit-CSV-Anforderungen, Abschnitt 1](revit-csv-integration-requirements.md). + Phase 14.0 ist abgeschlossen und freigegeben. Dieses Dokument beschreibt den gegen den aktuellen Code geprüften Zielzuschnitt. Phase 14.1 ist abgeschlossen. Phase 14.2 hat mit den externen Domänenverträgen, Identitätsregeln und dem diff --git a/dynamo/01_check_model_identity.py b/dynamo/01_check_model_identity.py deleted file mode 100644 index a3fe009..0000000 --- a/dynamo/01_check_model_identity.py +++ /dev/null @@ -1,385 +0,0 @@ -"""Read-only Revit 2026 model-identity diagnostics for a Dynamo Python node. - -Optional Dynamo input: - IN[0]: output directory or complete .json file path - -The script intentionally performs no Revit transaction and changes no model -data. OUT contains a compact summary plus the complete report. -""" - -import datetime -import json -import os -import platform -import sys -import tempfile -import traceback - -import clr - -clr.AddReference("RevitAPI") -clr.AddReference("RevitServices") - -from Autodesk.Revit.DB import ModelPathUtils, StorageType # noqa: E402 -from RevitServices.Persistence import DocumentManager # noqa: E402 - - -CHECKED_PARAMETER_NAMES = ("LB_ModelId", "LB_ProjectId") - - -def safe_text(value): - if value is None: - return None - try: - return str(value) - except Exception: - return None - - -def element_id_text(element_id): - if element_id is None: - return None - try: - return str(element_id.Value) - except Exception: - try: - return str(element_id.IntegerValue) - except Exception: - return safe_text(element_id) - - -def forge_type_id_text(value): - if value is None: - return None - try: - return value.TypeId - except Exception: - return safe_text(value) - - -def parameter_value(parameter): - result = { - "hasValue": False, - "raw": None, - "display": None, - } - try: - result["hasValue"] = bool(parameter.HasValue) - except Exception: - pass - - try: - storage_type = parameter.StorageType - if storage_type == StorageType.String: - result["raw"] = parameter.AsString() - elif storage_type == StorageType.Integer: - result["raw"] = int(parameter.AsInteger()) - elif storage_type == StorageType.Double: - result["raw"] = float(parameter.AsDouble()) - elif storage_type == StorageType.ElementId: - result["raw"] = element_id_text(parameter.AsElementId()) - except Exception as error: - result["readError"] = safe_text(error) - - try: - result["display"] = parameter.AsValueString() - except Exception: - pass - return result - - -def describe_parameter(parameter): - definition = None - try: - definition = parameter.Definition - except Exception: - pass - - name = None - if definition is not None: - try: - name = definition.Name - except Exception: - pass - - is_shared = False - try: - is_shared = bool(parameter.IsShared) - except Exception: - pass - - shared_guid = None - if is_shared: - try: - shared_guid = str(parameter.GUID) - except Exception: - pass - - data_type = None - group_type = None - if definition is not None: - try: - data_type = forge_type_id_text(definition.GetDataType()) - except Exception: - pass - try: - group_type = forge_type_id_text(definition.GetGroupTypeId()) - except Exception: - pass - - unit_type = None - try: - unit_type = forge_type_id_text(parameter.GetUnitTypeId()) - except Exception: - pass - - try: - storage_type = str(parameter.StorageType) - except Exception: - storage_type = None - - try: - is_read_only = bool(parameter.IsReadOnly) - except Exception: - is_read_only = None - - try: - user_modifiable = bool(parameter.UserModifiable) - except Exception: - user_modifiable = None - - return { - "name": name, - "parameterId": element_id_text(getattr(parameter, "Id", None)), - "isShared": is_shared, - "sharedGuid": shared_guid, - "storageType": storage_type, - "dataTypeId": data_type, - "groupTypeId": group_type, - "unitTypeId": unit_type, - "isReadOnly": is_read_only, - "userModifiable": user_modifiable, - "value": parameter_value(parameter), - } - - -def sorted_parameters(element): - parameters = [] - try: - parameters = [describe_parameter(parameter) for parameter in element.Parameters] - except Exception: - return [] - return sorted( - parameters, - key=lambda parameter: ( - (parameter.get("name") or "").casefold(), - parameter.get("parameterId") or "", - ), - ) - - -def named_parameter_occurrences(element, parameter_name): - result = [] - try: - parameters = element.GetParameters(parameter_name) - if parameters is not None: - result = [describe_parameter(parameter) for parameter in parameters] - except Exception: - parameter = None - try: - parameter = element.LookupParameter(parameter_name) - except Exception: - pass - if parameter is not None: - result = [describe_parameter(parameter)] - return result - - -def loaded_assembly_versions(): - result = {} - try: - from System import AppDomain - - for assembly in AppDomain.CurrentDomain.GetAssemblies(): - try: - name = assembly.GetName() - simple_name = str(name.Name) - if simple_name in ( - "DynamoCore", - "DynamoCoreWpf", - "DynamoRevitDS", - "RevitAPI", - "RevitServices", - ): - result[simple_name] = str(name.Version) - except Exception: - continue - except Exception: - pass - return dict(sorted(result.items())) - - -def get_cloud_identity(document): - result = {"isModelInCloud": False} - try: - result["isModelInCloud"] = bool(document.IsModelInCloud) - except Exception: - return result - if not result["isModelInCloud"]: - return result - - try: - model_path = document.GetCloudModelPath() - result["userVisiblePath"] = ModelPathUtils.ConvertModelPathToUserVisiblePath( - model_path - ) - for property_name, output_name in ( - ("GetProjectGUID", "projectGuid"), - ("GetModelGUID", "modelGuid"), - ): - try: - result[output_name] = str(getattr(model_path, property_name)()) - except Exception: - pass - except Exception as error: - result["readError"] = safe_text(error) - return result - - -def get_worksharing_identity(document): - result = {"isWorkshared": False} - try: - result["isWorkshared"] = bool(document.IsWorkshared) - except Exception: - return result - if not result["isWorkshared"]: - return result - - try: - model_path = document.GetWorksharingCentralModelPath() - result["centralModelPath"] = ModelPathUtils.ConvertModelPathToUserVisiblePath( - model_path - ) - except Exception as error: - result["readError"] = safe_text(error) - return result - - -def resolve_output_path(configured_path, report_name): - timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - default_directory = os.path.join(tempfile.gettempdir(), "leistungsbilanz-dynamo") - raw_path = safe_text(configured_path) - if raw_path is None or not raw_path.strip(): - directory = default_directory - file_path = os.path.join(directory, report_name + "-" + timestamp + ".json") - else: - expanded = os.path.abspath(os.path.expandvars(os.path.expanduser(raw_path.strip()))) - if expanded.lower().endswith(".json"): - file_path = expanded - directory = os.path.dirname(file_path) - else: - directory = expanded - file_path = os.path.join(directory, report_name + "-" + timestamp + ".json") - if not directory: - directory = os.getcwd() - if not os.path.isdir(directory): - os.makedirs(directory) - return file_path - - -def write_json(file_path, payload): - temporary_path = file_path + ".tmp" - with open(temporary_path, "w", encoding="utf-8", newline="\n") as output: - json.dump(payload, output, ensure_ascii=False, indent=2, sort_keys=True) - output.write("\n") - os.replace(temporary_path, file_path) - - -def get_input(index, default=None): - values = globals().get("IN", []) - try: - value = values[index] - return default if value is None else value - except Exception: - return default - - -def build_report(): - document = DocumentManager.Instance.CurrentDBDocument - if document is None: - raise RuntimeError("No active Revit document is available.") - - project_information = document.ProjectInformation - if project_information is None: - raise RuntimeError("The active document has no Project Information element.") - - application = document.Application - checked_parameters = { - name: named_parameter_occurrences(project_information, name) - for name in CHECKED_PARAMETER_NAMES - } - warnings = [] - for name in CHECKED_PARAMETER_NAMES: - occurrences = checked_parameters[name] - populated = [ - parameter - for parameter in occurrences - if parameter.get("value", {}).get("raw") not in (None, "") - ] - if not occurrences: - warnings.append(name + " is not bound to Project Information.") - elif not populated: - warnings.append(name + " exists but has no value on Project Information.") - elif len(occurrences) > 1: - warnings.append(name + " occurs more than once; use a shared-parameter GUID later.") - - return { - "reportSchemaVersion": 1, - "generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), - "readOnly": True, - "environment": { - "revitVersionNumber": safe_text(getattr(application, "VersionNumber", None)), - "revitVersionName": safe_text(getattr(application, "VersionName", None)), - "revitSubVersionNumber": safe_text( - getattr(application, "SubVersionNumber", None) - ), - "pythonImplementation": safe_text(getattr(sys.implementation, "name", None)), - "pythonVersion": platform.python_version(), - "assemblies": loaded_assembly_versions(), - }, - "document": { - "title": safe_text(document.Title), - "pathName": safe_text(document.PathName), - "isFamilyDocument": bool(document.IsFamilyDocument), - "cloud": get_cloud_identity(document), - "worksharing": get_worksharing_identity(document), - }, - "modelIdentityCandidates": { - "projectInformationUniqueId": safe_text(project_information.UniqueId), - "projectInformationElementId": element_id_text(project_information.Id), - "checkedProjectParameters": checked_parameters, - }, - "projectInformationParameters": sorted_parameters(project_information), - "warnings": warnings, - } - - -try: - report = build_report() - output_path = resolve_output_path(get_input(0), "model-identity") - write_json(output_path, report) - OUT = { - "ok": True, - "filePath": output_path, - "projectInformationUniqueId": report["modelIdentityCandidates"][ - "projectInformationUniqueId" - ], - "warnings": report["warnings"], - "report": report, - } -except Exception as error: - OUT = { - "ok": False, - "error": safe_text(error), - "traceback": traceback.format_exc(), - } diff --git a/dynamo/02_export_electrical_fixture_parameter_inventory.py b/dynamo/02_export_electrical_fixture_parameter_inventory.py deleted file mode 100644 index 440a222..0000000 --- a/dynamo/02_export_electrical_fixture_parameter_inventory.py +++ /dev/null @@ -1,517 +0,0 @@ -"""Export all Electrical Fixtures instance/type parameters from Revit 2026. - -Optional Dynamo inputs: - IN[0]: output directory or complete .json file path - IN[1]: include empty parameters (default True) - IN[2]: maximum aggregated sample values (default 5) - -The script is read-only and performs no Revit transaction. -""" - -import datetime -import json -import os -import platform -import sys -import tempfile -import traceback - -import clr - -clr.AddReference("RevitAPI") -clr.AddReference("RevitServices") - -from Autodesk.Revit.DB import ( # noqa: E402 - BuiltInCategory, - FilteredElementCollector, - ModelPathUtils, - StorageType, -) -from RevitServices.Persistence import DocumentManager # noqa: E402 - - -def safe_text(value): - if value is None: - return None - try: - return str(value) - except Exception: - return None - - -def element_id_text(element_id): - if element_id is None: - return None - try: - return str(element_id.Value) - except Exception: - try: - return str(element_id.IntegerValue) - except Exception: - return safe_text(element_id) - - -def forge_type_id_text(value): - if value is None: - return None - try: - return value.TypeId - except Exception: - return safe_text(value) - - -def parameter_value(parameter): - result = {"hasValue": False, "raw": None, "display": None} - try: - result["hasValue"] = bool(parameter.HasValue) - except Exception: - pass - - try: - storage_type = parameter.StorageType - if storage_type == StorageType.String: - result["raw"] = parameter.AsString() - elif storage_type == StorageType.Integer: - result["raw"] = int(parameter.AsInteger()) - elif storage_type == StorageType.Double: - result["raw"] = float(parameter.AsDouble()) - elif storage_type == StorageType.ElementId: - result["raw"] = element_id_text(parameter.AsElementId()) - except Exception as error: - result["readError"] = safe_text(error) - - try: - result["display"] = parameter.AsValueString() - except Exception: - pass - return result - - -def describe_parameter(parameter, scope): - definition = None - try: - definition = parameter.Definition - except Exception: - pass - - name = None - data_type = None - group_type = None - if definition is not None: - try: - name = definition.Name - except Exception: - pass - try: - data_type = forge_type_id_text(definition.GetDataType()) - except Exception: - pass - try: - group_type = forge_type_id_text(definition.GetGroupTypeId()) - except Exception: - pass - - is_shared = False - try: - is_shared = bool(parameter.IsShared) - except Exception: - pass - - shared_guid = None - if is_shared: - try: - shared_guid = str(parameter.GUID) - except Exception: - pass - - unit_type = None - try: - unit_type = forge_type_id_text(parameter.GetUnitTypeId()) - except Exception: - pass - - try: - storage_type = str(parameter.StorageType) - except Exception: - storage_type = None - - try: - is_read_only = bool(parameter.IsReadOnly) - except Exception: - is_read_only = None - - try: - user_modifiable = bool(parameter.UserModifiable) - except Exception: - user_modifiable = None - - return { - "scope": scope, - "name": name, - "parameterId": element_id_text(getattr(parameter, "Id", None)), - "isShared": is_shared, - "sharedGuid": shared_guid, - "storageType": storage_type, - "dataTypeId": data_type, - "groupTypeId": group_type, - "unitTypeId": unit_type, - "isReadOnly": is_read_only, - "userModifiable": user_modifiable, - "value": parameter_value(parameter), - } - - -def has_meaningful_value(parameter_description): - value = parameter_description.get("value", {}) - return bool(value.get("hasValue")) or value.get("raw") not in (None, "") or value.get( - "display" - ) not in (None, "") - - -def read_parameters(element, scope, include_empty): - result = [] - try: - for parameter in element.Parameters: - description = describe_parameter(parameter, scope) - if include_empty or has_meaningful_value(description): - result.append(description) - except Exception as error: - return [], [safe_text(error)] - result.sort( - key=lambda parameter: ( - (parameter.get("name") or "").casefold(), - parameter.get("parameterId") or "", - ) - ) - return result, [] - - -def read_space(element, document): - try: - space = element.Space - except Exception as error: - return None, safe_text(error) - if space is None: - return None, None - - level_name = None - try: - level = document.GetElement(space.LevelId) - level_name = None if level is None else safe_text(level.Name) - except Exception: - pass - - return { - "uniqueId": safe_text(space.UniqueId), - "elementId": element_id_text(space.Id), - "number": safe_text(getattr(space, "Number", None)), - "name": safe_text(getattr(space, "Name", None)), - "levelName": level_name, - }, None - - -def read_family_identity(element, document): - symbol = None - try: - symbol = element.Symbol - except Exception: - try: - symbol = document.GetElement(element.GetTypeId()) - except Exception: - pass - - family_name = None - type_name = None - type_unique_id = None - type_element_id = None - if symbol is not None: - try: - family_name = safe_text(symbol.Family.Name) - except Exception: - family_name = safe_text(getattr(symbol, "FamilyName", None)) - type_name = safe_text(getattr(symbol, "Name", None)) - type_unique_id = safe_text(getattr(symbol, "UniqueId", None)) - type_element_id = element_id_text(getattr(symbol, "Id", None)) - - return { - "familyName": family_name, - "typeName": type_name, - "typeUniqueId": type_unique_id, - "typeElementId": type_element_id, - }, symbol - - -def parameter_inventory_key(parameter): - stable_id = parameter.get("sharedGuid") or parameter.get("parameterId") or "" - return "|".join( - ( - parameter.get("scope") or "", - stable_id, - parameter.get("name") or "", - parameter.get("dataTypeId") or "", - ) - ) - - -def sample_value_key(value): - return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) - - -def add_to_inventory(inventory, parameter, max_samples): - key = parameter_inventory_key(parameter) - entry = inventory.get(key) - if entry is None: - entry = { - "scope": parameter.get("scope"), - "name": parameter.get("name"), - "parameterId": parameter.get("parameterId"), - "isShared": parameter.get("isShared"), - "sharedGuid": parameter.get("sharedGuid"), - "storageType": parameter.get("storageType"), - "dataTypeId": parameter.get("dataTypeId"), - "groupTypeId": parameter.get("groupTypeId"), - "unitTypeId": parameter.get("unitTypeId"), - "occurrenceCount": 0, - "populatedCount": 0, - "sampleValues": [], - "_sampleKeys": set(), - } - inventory[key] = entry - entry["occurrenceCount"] += 1 - if has_meaningful_value(parameter): - entry["populatedCount"] += 1 - value = parameter.get("value") - value_key = sample_value_key(value) - if len(entry["sampleValues"]) < max_samples and value_key not in entry["_sampleKeys"]: - entry["_sampleKeys"].add(value_key) - entry["sampleValues"].append(value) - - -def finalize_inventory(inventory): - result = [] - for entry in inventory.values(): - clean_entry = dict(entry) - clean_entry.pop("_sampleKeys", None) - result.append(clean_entry) - return sorted( - result, - key=lambda entry: ( - entry.get("scope") or "", - (entry.get("name") or "").casefold(), - entry.get("sharedGuid") or entry.get("parameterId") or "", - ), - ) - - -def loaded_assembly_versions(): - result = {} - try: - from System import AppDomain - - for assembly in AppDomain.CurrentDomain.GetAssemblies(): - try: - name = assembly.GetName() - simple_name = str(name.Name) - if simple_name in ( - "DynamoCore", - "DynamoCoreWpf", - "DynamoRevitDS", - "RevitAPI", - "RevitServices", - ): - result[simple_name] = str(name.Version) - except Exception: - continue - except Exception: - pass - return dict(sorted(result.items())) - - -def resolve_output_path(configured_path): - timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - default_directory = os.path.join(tempfile.gettempdir(), "leistungsbilanz-dynamo") - raw_path = safe_text(configured_path) - if raw_path is None or not raw_path.strip(): - directory = default_directory - file_path = os.path.join( - directory, "electrical-fixture-parameters-" + timestamp + ".json" - ) - else: - expanded = os.path.abspath(os.path.expandvars(os.path.expanduser(raw_path.strip()))) - if expanded.lower().endswith(".json"): - file_path = expanded - directory = os.path.dirname(file_path) - else: - directory = expanded - file_path = os.path.join( - directory, "electrical-fixture-parameters-" + timestamp + ".json" - ) - if not directory: - directory = os.getcwd() - if not os.path.isdir(directory): - os.makedirs(directory) - return file_path - - -def write_json(file_path, payload): - temporary_path = file_path + ".tmp" - with open(temporary_path, "w", encoding="utf-8", newline="\n") as output: - json.dump(payload, output, ensure_ascii=False, indent=2, sort_keys=True) - output.write("\n") - os.replace(temporary_path, file_path) - - -def get_input(index, default=None): - values = globals().get("IN", []) - try: - value = values[index] - return default if value is None else value - except Exception: - return default - - -def build_report(include_empty, max_samples): - document = DocumentManager.Instance.CurrentDBDocument - if document is None: - raise RuntimeError("No active Revit document is available.") - if document.IsFamilyDocument: - raise RuntimeError("Open a Revit project document, not a family document.") - - application = document.Application - collector = ( - FilteredElementCollector(document) - .OfCategory(BuiltInCategory.OST_ElectricalFixtures) - .WhereElementIsNotElementType() - ) - source_elements = list(collector) - source_elements.sort(key=lambda element: safe_text(element.UniqueId) or "") - - elements = [] - types_by_unique_id = {} - inventory = {} - errors = [] - elements_without_space = 0 - - for element in source_elements: - element_errors = [] - try: - family_identity, symbol = read_family_identity(element, document) - instance_parameters, parameter_errors = read_parameters( - element, "instance", include_empty - ) - element_errors.extend(parameter_errors) - for parameter in instance_parameters: - add_to_inventory(inventory, parameter, max_samples) - - type_unique_id = family_identity.get("typeUniqueId") - if symbol is not None and type_unique_id and type_unique_id not in types_by_unique_id: - type_parameters, type_errors = read_parameters(symbol, "type", include_empty) - element_errors.extend(type_errors) - for parameter in type_parameters: - add_to_inventory(inventory, parameter, max_samples) - types_by_unique_id[type_unique_id] = { - **family_identity, - "parameters": type_parameters, - } - - space, space_error = read_space(element, document) - if space_error: - element_errors.append("MEP Space: " + space_error) - if space is None: - elements_without_space += 1 - - elements.append( - { - "uniqueId": safe_text(element.UniqueId), - "elementId": element_id_text(element.Id), - "categoryName": safe_text( - None if element.Category is None else element.Category.Name - ), - "family": family_identity, - "space": space, - "instanceParameters": instance_parameters, - "warnings": element_errors, - } - ) - except Exception as error: - errors.append( - { - "uniqueId": safe_text(getattr(element, "UniqueId", None)), - "elementId": element_id_text(getattr(element, "Id", None)), - "error": safe_text(error), - } - ) - - return { - "reportSchemaVersion": 1, - "generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), - "readOnly": True, - "complete": len(errors) == 0, - "scope": { - "builtInCategory": "OST_ElectricalFixtures", - "wholeDocument": True, - "elementTypesExcluded": True, - "includeEmptyParameters": include_empty, - }, - "environment": { - "revitVersionNumber": safe_text(getattr(application, "VersionNumber", None)), - "revitVersionName": safe_text(getattr(application, "VersionName", None)), - "revitSubVersionNumber": safe_text( - getattr(application, "SubVersionNumber", None) - ), - "pythonImplementation": safe_text(getattr(sys.implementation, "name", None)), - "pythonVersion": platform.python_version(), - "assemblies": loaded_assembly_versions(), - }, - "document": { - "title": safe_text(document.Title), - "pathName": safe_text(document.PathName), - "projectInformationUniqueId": safe_text(document.ProjectInformation.UniqueId), - }, - "summary": { - "elementCount": len(source_elements), - "exportedElementCount": len(elements), - "typeCount": len(types_by_unique_id), - "parameterDefinitionCount": len(inventory), - "elementsWithoutMepSpace": elements_without_space, - "elementErrorCount": len(errors), - }, - "parameterInventory": finalize_inventory(inventory), - "types": sorted( - types_by_unique_id.values(), - key=lambda entry: ( - (entry.get("familyName") or "").casefold(), - (entry.get("typeName") or "").casefold(), - entry.get("typeUniqueId") or "", - ), - ), - "elements": elements, - "errors": errors, - } - - -try: - include_empty_input = get_input(1, True) - include_empty = bool(include_empty_input) - try: - max_samples = int(get_input(2, 5)) - except Exception: - max_samples = 5 - max_samples = max(0, min(max_samples, 50)) - - report = build_report(include_empty, max_samples) - output_path = resolve_output_path(get_input(0)) - write_json(output_path, report) - OUT = { - "ok": True, - "filePath": output_path, - "complete": report["complete"], - "summary": report["summary"], - "errors": report["errors"], - } -except Exception as error: - OUT = { - "ok": False, - "error": safe_text(error), - "traceback": traceback.format_exc(), - } diff --git a/dynamo/README.md b/dynamo/README.md deleted file mode 100644 index 800eded..0000000 --- a/dynamo/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Revit 2026 / Dynamo diagnostics - -This directory contains self-contained Python scripts for a Dynamo **Python -Script** node. They use only Dynamo's built-in Revit integration, the Revit API -and the Python standard library. No Dynamo package is required. - -The scripts are read-only. They do not start a Revit transaction and do not -change the open model. - -## Python engine - -Use the built-in `CPython3` engine in Revit 2026. Autodesk ships Dynamo with -Revit; optional PythonNet3 packages are not required by these diagnostics. - -## 01 - Check model identity - -File: `01_check_model_identity.py` - -The script reports: - -- Revit, Dynamo and Python versions; -- `ProjectInformation.UniqueId` as a native model-identity candidate; -- all occurrences and values of `LB_ModelId` and `LB_ProjectId` on Project - Information; -- all Project Information parameters; -- optional cloud/worksharing identity information when the API exposes it. - -Input `IN[0]` is optional. It may be either an output directory or a complete -`.json` file path. With no input, the report is written below the current -Windows temporary directory in `leistungsbilanz-dynamo`. - -## 02 - Inventory Electrical Fixtures parameters - -File: `02_export_electrical_fixture_parameter_inventory.py` - -The script reads every instance of -`BuiltInCategory.OST_ElectricalFixtures` in the complete current document. It -exports: - -- element, family, type and MEP Space identities; -- every instance parameter and value; -- every unique family-type parameter and value; -- an aggregated parameter inventory with occurrence counts and sample values; -- per-element warnings instead of aborting at the first unreadable element. - -Inputs: - -- `IN[0]` (optional): output directory or complete `.json` path; -- `IN[1]` (optional): include empty parameters, default `true`; -- `IN[2]` (optional): maximum sample values per aggregated parameter, default - `5`. - -The default output location is again the Windows temporary directory. The -generated report can contain model paths and project-specific parameter values; -review it before sharing or committing it. - -## Running a script - -1. Open the target model in Revit 2026. -2. Open Dynamo from **Manage > Visual Programming > Dynamo**. -3. Create a graph and add a **Python Script** node. -4. Select the `CPython3` engine for the node. -5. Copy the complete content of the desired `.py` file into the node. -6. Optionally connect a String node containing the output path to `IN[0]`. -7. Run the graph and inspect `OUT` for status, counts and the generated path. - -For the first test, run `01_check_model_identity.py` in the Revit main model. -Then run the parameter inventory. Keep both generated JSON files so their -structure can be checked before the production snapshot DTO is finalized.