mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 14:23:53 +00:00
Show resolved unit symbols in read-only Pset/Qto view; add write-back and fallback regression tests
Previously, unit symbols only appeared while a Pset/Qto was in edit mode
(pencil icon) -- the read-only summary view read raw {name: value} dicts
straight from ifcopenshell.util.element.get_psets(), a completely separate
path from the Attribute/unit_symbol machinery, so it never showed a label
even after the earlier fixes. This matters for the "someone in the field
just looking at values" use case, not just editing.
- bim/module/pset/data.py: switch to get_psets(verbose=True) to get each
property's own entity id, then resolve its unit symbol the same
override-aware way the edit-mode path does (tool.Pset.get_unit_symbol_for_prop).
Falls back gracefully (empty symbol) for IfcPreDefinedPropertySet
attributes, which aren't IfcProperty entities and can't carry a Unit
override.
- bim/module/pset/ui.py: read-only value button now shows "250 mm" instead
of just "250".
Also adds the regression tests planned but not yet committed:
- test/tool/test_pset.py: edit a property with its own Unit override and
write it back, confirming no rescale and the override survives.
- test/bim/test_prop.py (new): get_display_name() falls back to the plain
name (no crash) when no unit is resolvable or the project has no units
assigned at all.
This commit is contained in:
@@ -236,11 +236,9 @@ def import_attribute(
|
|||||||
elif data_type == "integer":
|
elif data_type == "integer":
|
||||||
new.int_value = 0 if new.is_null else int(data[attribute.name()])
|
new.int_value = 0 if new.is_null else int(data[attribute.name()])
|
||||||
elif data_type == "float":
|
elif data_type == "float":
|
||||||
attribute_type = attribute.type_of_attribute()
|
measure_class = attribute.type_of_attribute().declared_type().name()
|
||||||
if attribute_type._is("IfcLengthMeasure"):
|
new.special_type = tool.Pset.get_special_type_for_measure_class(measure_class)
|
||||||
new.special_type = "LENGTH"
|
new.unit_symbol = tool.Pset.get_unit_symbol_for_special_type(new.special_type, tool.Ifc.get())
|
||||||
elif attribute_type._is("IfcForceMeasure"):
|
|
||||||
new.special_type = "FORCE"
|
|
||||||
new.float_value = 0.0 if new.is_null else float(data[attribute.name()])
|
new.float_value = 0.0 if new.is_null else float(data[attribute.name()])
|
||||||
elif data_type == "enum":
|
elif data_type == "enum":
|
||||||
attribute_type = attribute.type_of_attribute()
|
attribute_type = attribute.type_of_attribute()
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class Data:
|
|||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
results = []
|
results = []
|
||||||
psetqtos = ifcopenshell.util.element.get_psets(
|
psetqtos = ifcopenshell.util.element.get_psets(
|
||||||
element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False
|
element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False, verbose=True
|
||||||
)
|
)
|
||||||
for name, data in sorted(psetqtos.items()):
|
for name, data in sorted(psetqtos.items()):
|
||||||
pset = ifc_file.by_id(data["id"])
|
pset = ifc_file.by_id(data["id"])
|
||||||
@@ -69,13 +69,28 @@ class Data:
|
|||||||
"id": data["id"],
|
"id": data["id"],
|
||||||
"Name": name,
|
"Name": name,
|
||||||
"is_expanded": is_expanded.get(data["id"], True),
|
"is_expanded": is_expanded.get(data["id"], True),
|
||||||
"Properties": [{"Name": k, "NominalValue": v} for k, v in sorted(data.items()) if k != "id"],
|
"Properties": [
|
||||||
|
cls.property_display_data(ifc_file, k, v) for k, v in sorted(data.items()) if k != "id"
|
||||||
|
],
|
||||||
"shared_pset_uses": len(pset_uses),
|
"shared_pset_uses": len(pset_uses),
|
||||||
"has_template": has_template,
|
"has_template": has_template,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return sorted(results, key=lambda v: v["Name"])
|
return sorted(results, key=lambda v: v["Name"])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def property_display_data(cls, ifc_file: ifcopenshell.file, name: str, verbose_value: Any) -> dict[str, Any]:
|
||||||
|
# Predefined property sets (e.g. IfcDoorPanelProperties) expose plain
|
||||||
|
# attribute values even in verbose mode, since they're typed IFC
|
||||||
|
# attributes rather than IfcProperty entities with their own id/Unit.
|
||||||
|
if not isinstance(verbose_value, dict):
|
||||||
|
return {"Name": name, "NominalValue": verbose_value, "UnitSymbol": ""}
|
||||||
|
|
||||||
|
unit_symbol = ""
|
||||||
|
if (prop_id := verbose_value.get("id")) and (prop_entity := ifc_file.by_id(prop_id)):
|
||||||
|
unit_symbol = tool.Pset.get_unit_symbol_for_prop(prop_entity, ifc_file)
|
||||||
|
return {"Name": name, "NominalValue": verbose_value["value"], "UnitSymbol": unit_symbol}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def format_pset_enum(cls, psets):
|
def format_pset_enum(cls, psets):
|
||||||
enum_items = []
|
enum_items = []
|
||||||
|
|||||||
@@ -203,9 +203,10 @@ def draw_psetqto_ui(
|
|||||||
row = box.row(align=True)
|
row = box.row(align=True)
|
||||||
row.scale_y = 0.8
|
row.scale_y = 0.8
|
||||||
row.label(text=prop["Name"])
|
row.label(text=prop["Name"])
|
||||||
op = row.operator(
|
display_value = get_display_value(nominal_value)
|
||||||
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
|
if unit_symbol := prop["UnitSymbol"]:
|
||||||
)
|
display_value = f"{display_value} {unit_symbol}"
|
||||||
|
op = row.operator("bim.select_similar", text=display_value, icon="NONE", emboss=False)
|
||||||
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
|
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
|
||||||
# calculate sum of all selected objects
|
# calculate sum of all selected objects
|
||||||
if active_operator:
|
if active_operator:
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import os
|
|||||||
from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args
|
from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell.util.unit
|
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
CollectionProperty,
|
CollectionProperty,
|
||||||
@@ -250,44 +249,19 @@ def set_numerical_value(self: "Attribute", value_name: str, new_value: Union[flo
|
|||||||
self[value_name] = new_value
|
self[value_name] = new_value
|
||||||
|
|
||||||
|
|
||||||
def get_length_value(self: "Attribute") -> float:
|
|
||||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
||||||
return self.float_value * si_conversion
|
|
||||||
|
|
||||||
|
|
||||||
def set_length_value(self: "Attribute", value: float) -> None:
|
|
||||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
||||||
self.float_value = value / si_conversion
|
|
||||||
|
|
||||||
|
|
||||||
def get_display_name(self: "Attribute") -> str:
|
def get_display_name(self: "Attribute") -> str:
|
||||||
DISPLAY_UNIT_TYPES = ("AREA", "VOLUME", "FORCE")
|
|
||||||
name = self.name
|
name = self.name
|
||||||
if not self.special_type or self.special_type not in DISPLAY_UNIT_TYPES:
|
if not self.unit_symbol:
|
||||||
return name
|
return name
|
||||||
|
return f"{name}, {self.unit_symbol}"
|
||||||
unit_type = f"{self.special_type}UNIT"
|
|
||||||
project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), unit_type)
|
|
||||||
if not project_unit:
|
|
||||||
return name
|
|
||||||
|
|
||||||
unit_symbol = ifcopenshell.util.unit.get_unit_symbol(project_unit)
|
|
||||||
return f"{name}, {unit_symbol}"
|
|
||||||
|
|
||||||
|
|
||||||
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
|
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
|
||||||
AttributeSpecialType = Literal[
|
# Either "", "DATE", "DATETIME", "LOGICAL", "URI", "DURATION", or an
|
||||||
"",
|
# IfcUnitEnum/IfcDerivedUnitEnum value with the "UNIT" suffix stripped (e.g.
|
||||||
"DATE",
|
# "LENGTH", "PRESSURE", "MODULUSOFELASTICITY") as returned by
|
||||||
"DATETIME",
|
# tool.Pset.get_special_type_for_prop().
|
||||||
"LENGTH",
|
AttributeSpecialType = str
|
||||||
"AREA",
|
|
||||||
"VOLUME",
|
|
||||||
"FORCE",
|
|
||||||
"LOGICAL",
|
|
||||||
"URI",
|
|
||||||
"DURATION",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Attribute(PropertyGroup):
|
class Attribute(PropertyGroup):
|
||||||
@@ -318,9 +292,6 @@ class Attribute(PropertyGroup):
|
|||||||
get=lambda self: float(self.get("float_value", 0.0)),
|
get=lambda self: float(self.get("float_value", 0.0)),
|
||||||
set=set_float_value,
|
set=set_float_value,
|
||||||
)
|
)
|
||||||
length_value: FloatProperty(
|
|
||||||
name="Value", description=tooltip, get=get_length_value, set=set_length_value, unit="LENGTH"
|
|
||||||
)
|
|
||||||
enum_items: StringProperty(name="Value")
|
enum_items: StringProperty(name="Value")
|
||||||
"""Json serialized mapping of enum items:
|
"""Json serialized mapping of enum items:
|
||||||
Typically a dictionary of string identifiers to item names.
|
Typically a dictionary of string identifiers to item names.
|
||||||
@@ -342,6 +313,7 @@ class Attribute(PropertyGroup):
|
|||||||
value_max: FloatProperty(description="This is used to validate int_value and float_value")
|
value_max: FloatProperty(description="This is used to validate int_value and float_value")
|
||||||
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
|
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
|
||||||
special_type: StringProperty(name="Special Value Type", default="")
|
special_type: StringProperty(name="Special Value Type", default="")
|
||||||
|
unit_symbol: StringProperty(name="Unit Symbol", default="")
|
||||||
use_explorer_ui: BoolProperty()
|
use_explorer_ui: BoolProperty()
|
||||||
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
|
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
|
||||||
update: StringProperty(name="Update", description="Custom update function to be executed")
|
update: StringProperty(name="Update", description="Custom update function to be executed")
|
||||||
@@ -357,7 +329,6 @@ class Attribute(PropertyGroup):
|
|||||||
bool_value: bool
|
bool_value: bool
|
||||||
int_value: int
|
int_value: int
|
||||||
float_value: float
|
float_value: float
|
||||||
length_value: float
|
|
||||||
enum_items: str
|
enum_items: str
|
||||||
enum_items_dynamic: str
|
enum_items_dynamic: str
|
||||||
enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||||
@@ -373,6 +344,7 @@ class Attribute(PropertyGroup):
|
|||||||
value_min_constraint: bool
|
value_min_constraint: bool
|
||||||
value_max: float
|
value_max: float
|
||||||
value_max_constraint: bool
|
value_max_constraint: bool
|
||||||
|
unit_symbol: str
|
||||||
use_explorer_ui: bool
|
use_explorer_ui: bool
|
||||||
metadata: str
|
metadata: str
|
||||||
update: str
|
update: str
|
||||||
@@ -430,8 +402,6 @@ class Attribute(PropertyGroup):
|
|||||||
elif data_type == "integer":
|
elif data_type == "integer":
|
||||||
return "int_value"
|
return "int_value"
|
||||||
elif data_type == "float":
|
elif data_type == "float":
|
||||||
if display_only and self.special_type == "LENGTH":
|
|
||||||
return "length_value"
|
|
||||||
return "float_value"
|
return "float_value"
|
||||||
elif data_type == "enum":
|
elif data_type == "enum":
|
||||||
return "enum_value"
|
return "enum_value"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import ifcopenshell
|
|||||||
import ifcopenshell.api.pset
|
import ifcopenshell.api.pset
|
||||||
import ifcopenshell.util.attribute
|
import ifcopenshell.util.attribute
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.util.unit
|
||||||
|
|
||||||
import bonsai.bim.helper
|
import bonsai.bim.helper
|
||||||
import bonsai.bim.schema
|
import bonsai.bim.schema
|
||||||
@@ -168,42 +169,72 @@ class Pset(bonsai.core.tool.Pset):
|
|||||||
pset_id=0, pset_name=cls.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
|
pset_id=0, pset_name=cls.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Templates for quantities can specify their kind via TemplateType (e.g.
|
||||||
|
# "Q_LENGTH") instead of PrimaryMeasureType. IfcQuantityCount has no
|
||||||
|
# associated measure/unit, so it is intentionally absent here.
|
||||||
|
QUANTITY_TEMPLATE_TYPE_TO_SPECIAL_TYPE = {
|
||||||
|
"Q_LENGTH": "LENGTH",
|
||||||
|
"Q_AREA": "AREA",
|
||||||
|
"Q_VOLUME": "VOLUME",
|
||||||
|
"Q_WEIGHT": "MASS",
|
||||||
|
"Q_TIME": "TIME",
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_special_type_for_prop(
|
def get_special_type_for_measure_class(cls, measure_class: str) -> str:
|
||||||
cls, prop_or_prop_template: ifcopenshell.entity_instance
|
"""Get the ``special_type`` (an IfcUnitEnum value with "UNIT" stripped) for an IFC measure class.
|
||||||
) -> Literal["LENGTH"] | Literal["AREA"] | Literal["VOLUME"] | Literal["URI"] | Literal[""]:
|
|
||||||
special_type = ""
|
:param measure_class: An IFC measure class name, e.g. "IfcLengthMeasure".
|
||||||
|
:return: E.g. "LENGTH", or "" if the class has no associated unit type.
|
||||||
|
"""
|
||||||
|
if not measure_class.endswith("Measure"):
|
||||||
|
return ""
|
||||||
|
unit_type = ifcopenshell.util.unit.get_measure_unit_type(measure_class)
|
||||||
|
return unit_type[: -len("UNIT")] if unit_type.endswith("UNIT") else ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_special_type_for_prop(cls, prop_or_prop_template: ifcopenshell.entity_instance) -> str:
|
||||||
|
"""Classify a property/quantity/template by its measure type.
|
||||||
|
|
||||||
|
:return: An IfcUnitEnum value with the "UNIT" suffix stripped (e.g.
|
||||||
|
"LENGTH", "PRESSURE"), "URI" for IfcURIReference, or "" if the
|
||||||
|
value has no associated unit type.
|
||||||
|
"""
|
||||||
if prop_or_prop_template.is_a("IfcPropertyTemplate"):
|
if prop_or_prop_template.is_a("IfcPropertyTemplate"):
|
||||||
primary_measure_type = prop_or_prop_template.PrimaryMeasureType
|
primary_measure_type = prop_or_prop_template.PrimaryMeasureType
|
||||||
template_type = prop_or_prop_template.TemplateType
|
if primary_measure_type == "IfcURIReference":
|
||||||
if primary_measure_type in ("IfcPositiveLengthMeasure", "IfcLengthMeasure") or template_type == "Q_LENGTH":
|
return "URI"
|
||||||
special_type = "LENGTH"
|
if primary_measure_type:
|
||||||
elif primary_measure_type == "IfcAreaMeasure" or template_type == "Q_AREA":
|
return cls.get_special_type_for_measure_class(primary_measure_type)
|
||||||
special_type = "AREA"
|
return cls.QUANTITY_TEMPLATE_TYPE_TO_SPECIAL_TYPE.get(prop_or_prop_template.TemplateType, "")
|
||||||
elif primary_measure_type == "IfcVolumeMeasure" or template_type == "Q_VOLUME":
|
elif prop_or_prop_template.is_a("IfcPropertySingleValue"):
|
||||||
special_type = "VOLUME"
|
value = prop_or_prop_template.NominalValue
|
||||||
elif primary_measure_type == "IfcURIReference":
|
if value is not None:
|
||||||
special_type = "URI"
|
return cls.get_special_type_for_measure_class(value.is_a())
|
||||||
else:
|
elif prop_or_prop_template.is_a("IfcPhysicalSimpleQuantity"):
|
||||||
if prop_or_prop_template.is_a("IfcPropertySingleValue"):
|
entity = prop_or_prop_template.wrapped_data.declaration().as_entity()
|
||||||
value = prop_or_prop_template.NominalValue
|
measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name()
|
||||||
if value is not None:
|
return cls.get_special_type_for_measure_class(measure_class)
|
||||||
value_type = value.is_a()
|
return ""
|
||||||
if value_type in ("IfcLengthMeasure", "IfcPositiveLengthMeasure"):
|
|
||||||
special_type = "LENGTH"
|
@classmethod
|
||||||
elif value_type == "IfcAreaMeasure":
|
def get_unit_symbol_for_special_type(cls, special_type: str, ifc_file: ifcopenshell.file) -> str:
|
||||||
special_type = "AREA"
|
"""Get the project's default unit symbol for a `special_type` (see `get_special_type_for_prop`).
|
||||||
elif value_type == "IfcVolumeMeasure":
|
|
||||||
special_type = "VOLUME"
|
Used where there's no property instance to check for a `Unit` override
|
||||||
elif prop_or_prop_template.is_a("IfcPhysicalSimpleQuantity"):
|
(e.g. a template, or a native IFC entity attribute, neither of which
|
||||||
prop_class = prop_or_prop_template.is_a()
|
can carry one).
|
||||||
if prop_class == "IfcQuantityArea":
|
"""
|
||||||
special_type = "AREA"
|
if not special_type or special_type == "URI":
|
||||||
elif prop_class == "IfcQuantityVolume":
|
return ""
|
||||||
special_type = "VOLUME"
|
unit = ifcopenshell.util.unit.get_project_unit(ifc_file, f"{special_type}UNIT")
|
||||||
elif prop_class == "IfcQuantityLength":
|
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
|
||||||
special_type = "LENGTH"
|
|
||||||
return special_type
|
@classmethod
|
||||||
|
def get_unit_symbol_for_prop(cls, prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file) -> str:
|
||||||
|
"""Get the unit symbol for an existing property/quantity, respecting its own `Unit` override."""
|
||||||
|
unit = ifcopenshell.util.unit.get_property_unit(prop, ifc_file)
|
||||||
|
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_pset_from_existing(
|
def import_pset_from_existing(
|
||||||
@@ -295,6 +326,7 @@ class Pset(bonsai.core.tool.Pset):
|
|||||||
metadata.is_null = value is None
|
metadata.is_null = value is None
|
||||||
metadata.is_optional = True
|
metadata.is_optional = True
|
||||||
metadata.special_type = cls.get_special_type_for_prop(prop)
|
metadata.special_type = cls.get_special_type_for_prop(prop)
|
||||||
|
metadata.unit_symbol = cls.get_unit_symbol_for_prop(prop, tool.Ifc.get())
|
||||||
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
||||||
process_prop_description(metadata)
|
process_prop_description(metadata)
|
||||||
|
|
||||||
@@ -383,6 +415,7 @@ class Pset(bonsai.core.tool.Pset):
|
|||||||
metadata.is_optional = True
|
metadata.is_optional = True
|
||||||
metadata.data_type = cls.get_prop_template_primitive_type(prop_template)
|
metadata.data_type = cls.get_prop_template_primitive_type(prop_template)
|
||||||
metadata.special_type = cls.get_special_type_for_prop(prop_template)
|
metadata.special_type = cls.get_special_type_for_prop(prop_template)
|
||||||
|
metadata.unit_symbol = cls.get_unit_symbol_for_special_type(metadata.special_type, tool.Ifc.get())
|
||||||
|
|
||||||
if metadata.data_type == "string":
|
if metadata.data_type == "string":
|
||||||
metadata.string_value = "" if metadata.is_null else str(data[prop_template.Name])
|
metadata.string_value = "" if metadata.is_null else str(data[prop_template.Name])
|
||||||
@@ -432,6 +465,8 @@ class Pset(bonsai.core.tool.Pset):
|
|||||||
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
|
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
|
||||||
|
|
||||||
elif prop_template.TemplateType.startswith("Q_"):
|
elif prop_template.TemplateType.startswith("Q_"):
|
||||||
|
if prop_data:
|
||||||
|
continue # Existing quantity will be added later by import_pset_from_existing.
|
||||||
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
|
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
|
||||||
|
|
||||||
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
|
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
|
||||||
@@ -493,6 +528,7 @@ class Pset(bonsai.core.tool.Pset):
|
|||||||
metadata.is_null = value is None
|
metadata.is_null = value is None
|
||||||
metadata.is_optional = True
|
metadata.is_optional = True
|
||||||
metadata.special_type = special_type
|
metadata.special_type = special_type
|
||||||
|
metadata.unit_symbol = cls.get_unit_symbol_for_special_type(special_type, tool.Ifc.get())
|
||||||
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -361,31 +361,22 @@ Scenario: Edit pset length property
|
|||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I press "mesh.add_stair"
|
And I press "mesh.add_stair"
|
||||||
And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Pset_StairFlightCommon').id()"
|
And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Pset_StairFlightCommon').id()"
|
||||||
And the variable "si_conversion" is "ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())"
|
|
||||||
And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||||
|
|
||||||
# Testing IfcPositiveLengthMeasure type of prop
|
# Testing IfcPositiveLengthMeasure type of prop
|
||||||
Then "active_object.PsetProperties.properties['TreadLength'].metadata.special_type" is "LENGTH"
|
Then "active_object.PsetProperties.properties['TreadLength'].metadata.special_type" is "LENGTH"
|
||||||
And "active_object.PsetProperties.properties['TreadLength'].metadata.float_value" is "250"
|
And "active_object.PsetProperties.properties['TreadLength'].metadata.float_value" is "250"
|
||||||
And "active_object.PsetProperties.properties['TreadLength'].metadata.length_value" is roughly "0.25"
|
|
||||||
|
|
||||||
When I set "active_object.PsetProperties.properties['TreadLength'].metadata.float_value" to "350"
|
When I set "active_object.PsetProperties.properties['TreadLength'].metadata.float_value" to "350"
|
||||||
Then "active_object.PsetProperties.properties['TreadLength'].metadata.float_value" is roughly "350"
|
Then "active_object.PsetProperties.properties['TreadLength'].metadata.float_value" is roughly "350"
|
||||||
|
|
||||||
When I set "active_object.PsetProperties.properties['TreadLength'].metadata.length_value" to "0.45"
|
|
||||||
Then "active_object.PsetProperties.properties['TreadLength'].metadata.float_value" is roughly "450"
|
|
||||||
|
|
||||||
# Testing IfcLengthMeasure type of prop
|
# Testing IfcLengthMeasure type of prop
|
||||||
Then "active_object.PsetProperties.properties['NosingLength'].metadata.special_type" is "LENGTH"
|
Then "active_object.PsetProperties.properties['NosingLength'].metadata.special_type" is "LENGTH"
|
||||||
And "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is "0.0"
|
And "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is "0.0"
|
||||||
And "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" is roughly "0.0"
|
|
||||||
|
|
||||||
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" to "350"
|
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" to "350"
|
||||||
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "350"
|
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "350"
|
||||||
|
|
||||||
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" to "0.45"
|
|
||||||
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "450"
|
|
||||||
|
|
||||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||||
Then nothing happens
|
Then nothing happens
|
||||||
|
|
||||||
@@ -394,19 +385,14 @@ Scenario: Edit qset length property
|
|||||||
And I press "mesh.add_stair"
|
And I press "mesh.add_stair"
|
||||||
And I press "bim.perform_quantity_take_off"
|
And I press "bim.perform_quantity_take_off"
|
||||||
And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Qto_StairFlightBaseQuantities').id()"
|
And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Qto_StairFlightBaseQuantities').id()"
|
||||||
And the variable "si_conversion" is "ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())"
|
|
||||||
And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||||
|
|
||||||
# Testing Q_LENGTH type of prop
|
# Testing Q_LENGTH type of prop
|
||||||
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
|
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
|
||||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2156.485"
|
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2156.485"
|
||||||
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.156"
|
|
||||||
|
|
||||||
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
|
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
|
||||||
Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35"
|
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350"
|
||||||
|
|
||||||
When I set "active_object.PsetProperties.properties['Length'].metadata.length_value" to "0.45"
|
|
||||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "450"
|
|
||||||
|
|
||||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||||
Then nothing happens
|
Then nothing happens
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Bonsai - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2026
|
||||||
|
#
|
||||||
|
# This file is part of Bonsai.
|
||||||
|
#
|
||||||
|
# Bonsai is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Bonsai is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import ifcopenshell
|
||||||
|
import ifcopenshell.api.pset
|
||||||
|
import ifcopenshell.api.root
|
||||||
|
import ifcopenshell.api.unit
|
||||||
|
|
||||||
|
import bonsai.tool as tool
|
||||||
|
from test.bim.bootstrap import NewFile
|
||||||
|
|
||||||
|
|
||||||
|
def import_single_property(ifc, element, prop):
|
||||||
|
"""Import a single existing IfcProperty into a real, addon-registered
|
||||||
|
PsetProperties collection, exactly as the property editor does, and
|
||||||
|
return its `metadata` (an `Attribute`)."""
|
||||||
|
pset = ifcopenshell.api.pset.add_pset(ifc, product=element, name="Pset_Test")
|
||||||
|
pset.HasProperties = [prop]
|
||||||
|
obj = bpy.data.objects.new(prop.Name, None)
|
||||||
|
tool.Ifc.link(element, obj)
|
||||||
|
props = obj.PsetProperties
|
||||||
|
tool.Pset.import_pset_from_existing(pset, props, None)
|
||||||
|
return props.properties[prop.Name].metadata
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetDisplayName(NewFile):
|
||||||
|
def test_appends_the_resolved_unit_symbol(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||||
|
pressure = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="PRESSUREUNIT")
|
||||||
|
ifcopenshell.api.unit.assign_unit(ifc, units=[pressure])
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
prop = ifc.createIfcPropertySingleValue(Name="Foo", NominalValue=ifc.createIfcPressureMeasure(5.0))
|
||||||
|
metadata = import_single_property(ifc, element, prop)
|
||||||
|
|
||||||
|
assert metadata.display_name == "Foo, Pa"
|
||||||
|
|
||||||
|
def test_falls_back_to_the_plain_name_when_no_unit_is_resolvable(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||||
|
# No units assigned to the project at all -- nothing to resolve.
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
prop = ifc.createIfcPropertySingleValue(Name="Foo", NominalValue=ifc.createIfcPressureMeasure(5.0))
|
||||||
|
metadata = import_single_property(ifc, element, prop)
|
||||||
|
|
||||||
|
assert metadata.unit_symbol == ""
|
||||||
|
assert metadata.display_name == "Foo"
|
||||||
|
|
||||||
|
def test_falls_back_to_the_plain_name_for_a_non_measure_property(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
prop = ifc.createIfcPropertySingleValue(Name="Foo", NominalValue=ifc.createIfcText("Bar"))
|
||||||
|
metadata = import_single_property(ifc, element, prop)
|
||||||
|
|
||||||
|
assert metadata.unit_symbol == ""
|
||||||
|
assert metadata.display_name == "Foo"
|
||||||
@@ -20,6 +20,8 @@ import bpy
|
|||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api
|
import ifcopenshell.api
|
||||||
import ifcopenshell.api.pset
|
import ifcopenshell.api.pset
|
||||||
|
import ifcopenshell.api.root
|
||||||
|
import ifcopenshell.api.unit
|
||||||
|
|
||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
@@ -52,3 +54,80 @@ class TestIsPsetEmpty(NewFile):
|
|||||||
assert subject.is_pset_empty(pset) is False
|
assert subject.is_pset_empty(pset) is False
|
||||||
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Foo": None})
|
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Foo": None})
|
||||||
assert subject.is_pset_empty(pset) is True
|
assert subject.is_pset_empty(pset) is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestEditingAnOverriddenUnitPropertyRoundTrips(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
# Project default is mm, but this property is authored directly in m.
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||||
|
length_mm = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||||
|
ifcopenshell.api.unit.assign_unit(ifc, units=[length_mm])
|
||||||
|
length_m = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="LENGTHUNIT")
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
pset = ifcopenshell.api.pset.add_pset(ifc, product=element, name="Pset_Test")
|
||||||
|
prop = ifc.createIfcPropertySingleValue(
|
||||||
|
Name="Foo", NominalValue=ifc.createIfcLengthMeasure(2.5), Unit=length_m
|
||||||
|
)
|
||||||
|
pset.HasProperties = [prop]
|
||||||
|
|
||||||
|
obj = bpy.data.objects.new("Wall", None)
|
||||||
|
tool.Ifc.link(element, obj)
|
||||||
|
blender_props = obj.PsetProperties
|
||||||
|
subject.import_pset_from_existing(pset, blender_props, None)
|
||||||
|
|
||||||
|
metadata = blender_props.properties["Foo"].metadata
|
||||||
|
assert metadata.unit_symbol == "m"
|
||||||
|
assert metadata.float_value == 2.5 # raw stored value, not rescaled to the project's mm
|
||||||
|
|
||||||
|
# Simulate a user edit in the property editor.
|
||||||
|
metadata.float_value = 3.5
|
||||||
|
|
||||||
|
# Simulate what EditPset.execute() does: collect the raw value straight
|
||||||
|
# off the metadata and write it back, with no rescaling step.
|
||||||
|
properties = {"Foo": metadata.get_value()}
|
||||||
|
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties=properties)
|
||||||
|
|
||||||
|
assert prop.NominalValue.wrappedValue == 3.5 # not rescaled to 3500mm
|
||||||
|
assert prop.Unit == length_m # override preserved
|
||||||
|
|
||||||
|
|
||||||
|
class TestImportingATemplatedQuantityRespectsItsOwnUnitOverride(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
# Regression test: import_pset_from_template's Q_ branch used to
|
||||||
|
# unconditionally re-template existing quantities, which shadowed
|
||||||
|
# their own Unit override with the project default -- edit mode
|
||||||
|
# showed "m" while the read-only panel correctly showed "mm".
|
||||||
|
# Project default is m, but this quantity is authored directly in mm.
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||||
|
length_m = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="LENGTHUNIT")
|
||||||
|
ifcopenshell.api.unit.assign_unit(ifc, units=[length_m])
|
||||||
|
length_mm = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||||
|
|
||||||
|
element = ifc.createIfcBeam()
|
||||||
|
qto = ifcopenshell.api.pset.add_qto(ifc, product=element, name="Qto_Test")
|
||||||
|
quantity = ifc.createIfcQuantityLength(Name="Foo", Unit=length_mm, LengthValue=2500.0)
|
||||||
|
qto.Quantities = [quantity]
|
||||||
|
|
||||||
|
pset_template = ifc.createIfcPropertySetTemplate(
|
||||||
|
Name="Qto_Test",
|
||||||
|
TemplateType="PSET_TYPEDRIVENOVERRIDE",
|
||||||
|
ApplicableEntity="IfcBeam",
|
||||||
|
HasPropertyTemplates=[ifc.createIfcSimplePropertyTemplate(Name="Foo", TemplateType="Q_LENGTH")],
|
||||||
|
)
|
||||||
|
|
||||||
|
obj = bpy.data.objects.new("Beam", None)
|
||||||
|
tool.Ifc.link(element, obj)
|
||||||
|
blender_props = obj.PsetProperties
|
||||||
|
# Mirrors core/pset.py's enable_pset_editing: template pass, then existing-data pass.
|
||||||
|
subject.import_pset_from_template(pset_template, qto, blender_props)
|
||||||
|
subject.import_pset_from_existing(qto, blender_props, pset_template)
|
||||||
|
|
||||||
|
assert len(blender_props.properties) == 1 # not duplicated by the template pass
|
||||||
|
metadata = blender_props.properties["Foo"].metadata
|
||||||
|
assert metadata.unit_symbol == "mm" # the quantity's own override, not the project default "m"
|
||||||
|
assert metadata.float_value == 2500.0 # raw stored value, not rescaled
|
||||||
|
|||||||
Reference in New Issue
Block a user