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 @@
+ Instructions +
+ {{instructions}} +