From 16fd614886e0a550aa385dc287628274b3ced650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mell=C3=BCh?= <74136980+c-mellueh@users.noreply.github.com> Date: Mon, 6 Oct 2025 06:48:45 +0200 Subject: [PATCH] Add instructions to IFCTester in Blender (#7195) * add instructions to blender ui * add requirement instructions to html * black . --- .../bonsai/bim/module/tester/operator.py | 1 + src/bonsai/bonsai/bim/module/tester/prop.py | 3 +++ src/bonsai/bonsai/bim/module/tester/ui.py | 23 ++++++++++++++-- src/bonsai/bonsai/bim/ui.py | 26 +++++++++++++++++++ src/ifctester/ifctester/reporter.py | 3 ++- src/ifctester/ifctester/templates/report.html | 8 ++++++ 6 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index 18b922f692..f7ff1aa744 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -484,6 +484,7 @@ class SelectRequirement(bpy.types.Operator): props.n_entities = len(failed_entities) props.has_entities = True if props.n_entities > 0 else False props.failed_entities.clear() + props.active_requirement_index = self.req_index for e in failed_entities: new_entity = props.failed_entities.add() diff --git a/src/bonsai/bonsai/bim/module/tester/prop.py b/src/bonsai/bonsai/bim/module/tester/prop.py index 4bc6b307d7..824542c96d 100644 --- a/src/bonsai/bonsai/bim/module/tester/prop.py +++ b/src/bonsai/bonsai/bim/module/tester/prop.py @@ -71,6 +71,8 @@ class IfcTesterProperties(PropertyGroup): generate_ods_report: BoolProperty(default=False, name="Generate ODS report", options=set()) flag: BoolProperty(default=False, name="Flag Failed Entities", options=set()) active_specification_index: IntProperty(name="Active Specification Index", update=update_active_specification_index) + active_requirement_index: IntProperty(name="Active Requirement Index") + old_index: IntProperty(name="", default=0) active_failed_entity_index: IntProperty(name="Active Failed Entity Index") specifications: CollectionProperty(name="Specifications", type=Specification) @@ -89,6 +91,7 @@ class IfcTesterProperties(PropertyGroup): generate_ods_report: bool flag: bool active_specification_index: int + active_requirement_index: int old_index: int active_failed_entity_index: int specifications: bpy.types.bpy_prop_collection_idprop[Specification] diff --git a/src/bonsai/bonsai/bim/module/tester/ui.py b/src/bonsai/bonsai/bim/module/tester/ui.py index 13477afec1..73d1bbc102 100644 --- a/src/bonsai/bonsai/bim/module/tester/ui.py +++ b/src/bonsai/bonsai/bim/module/tester/ui.py @@ -20,6 +20,8 @@ from __future__ import annotations from typing import TYPE_CHECKING +from bonsai.bim.ui import draw_multiline_text + import bpy from bpy.types import Panel, UIList import bonsai.tool as tool @@ -95,11 +97,11 @@ class BIM_PT_tester(Panel): "active_specification_index", ) - self.draw_editable_ui() + self.draw_editable_ui(context) row = self.layout.row() row.operator("bim.export_bcf", text="Export BCF", icon="EXPORT") - def draw_editable_ui(self) -> None: + def draw_editable_ui(self, context: bpy.types.Context) -> None: props = tool.Tester.get_tester_props() specification = TesterData.data["specification"] @@ -110,6 +112,13 @@ class BIM_PT_tester(Panel): row.label( text=f'Passed: {specification["total_checks_pass"]}/{specification["total_checks"]} ({specification["percent_checks_pass"]}%)' ) + row = self.layout.row() + if specification.get("instructions"): + row.label(text="Instructions:") + box = self.layout.box() + column = box.column(align=True) + draw_multiline_text(column, specification.get("instructions"), context=context) + row = self.layout.row() row.label(text=f"Requirements ({n_requirements}):") if props.flag: @@ -132,6 +141,16 @@ class BIM_PT_tester(Panel): and props.n_entities > 0 and len(props.failed_entities) > 0 ): + + requirement = specification["requirements"][props.active_requirement_index] + metadata = requirement.get("metadata") + if metadata and metadata.get("@instructions"): + row = self.layout.row() + row.label(text="Instructions:") + box = self.layout.box() + column = box.column(align=True) + draw_multiline_text(column, metadata.get("@instructions"), context=context) + row = self.layout.row() row.label(text=f"Failed entities [{props.n_entities}]:") self.layout.template_list( diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 16f674207b..aad6479c01 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -40,6 +40,7 @@ from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty from bonsai.bim.module.pset.prop import IfcProperty from typing import Optional, TYPE_CHECKING, Literal from natsort import natsorted +import textwrap if TYPE_CHECKING: @@ -1491,6 +1492,31 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) - url_op.uri = url +def draw_multiline_text( + layout: bpy.types.UILayout, + text: str, + *, + context: bpy.types.Context | None = None, +) -> None: + """Render a read-only text box that wraps long text.""" + assert layout + + region_width = 200 + if context and context.region: + region_width = context.region.width + + approximate_char_width = 7 # Empirical average width for Blender UI font (px) + wrap_width = max(20, int(region_width / approximate_char_width)) + + for paragraph in text.splitlines(): + if not paragraph: + layout.label(text="") + continue + + for line in textwrap.wrap(paragraph, width=wrap_width): + layout.label(text=line) + + class BIM_PT_decorators_overlay(Panel): bl_space_type = "VIEW_3D" bl_region_type = "HEADER" diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index b4b9929d88..90e0b7fc53 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -459,7 +459,8 @@ class Html(Json): requirement["total_passed_entities"] = total_passed_entities requirement["total_omitted_passes"] = total_passed_entities - self.entity_limit requirement["has_omitted_passes"] = total_passed_entities > self.entity_limit - + requirement["instructions"] = requirement["metadata"].get("@instructions") + def limit_entities(self, entities): if len(entities) > self.entity_limit: if entities[0]["element_type"]: diff --git a/src/ifctester/ifctester/templates/report.html b/src/ifctester/ifctester/templates/report.html index a6e3157961..4d94acb163 100644 --- a/src/ifctester/ifctester/templates/report.html +++ b/src/ifctester/ifctester/templates/report.html @@ -157,6 +157,14 @@ {{description}} + {{#instructions}} +
+

+ Instructions +

+ {{instructions}} +
+ {{/instructions}} {{#total_ckecks}}