mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-23 03:40:59 +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:
@@ -361,31 +361,22 @@ Scenario: Edit pset length property
|
||||
Given an empty IFC project
|
||||
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 "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')"
|
||||
|
||||
# Testing IfcPositiveLengthMeasure type of prop
|
||||
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.length_value" is roughly "0.25"
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
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.length_value" is roughly "0.0"
|
||||
|
||||
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"
|
||||
|
||||
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')"
|
||||
Then nothing happens
|
||||
|
||||
@@ -394,19 +385,14 @@ Scenario: Edit qset length property
|
||||
And I press "mesh.add_stair"
|
||||
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 "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')"
|
||||
|
||||
# Testing Q_LENGTH type of prop
|
||||
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.length_value" is roughly "2.156"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350"
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
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.api
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.unit
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
@@ -52,3 +54,80 @@ class TestIsPsetEmpty(NewFile):
|
||||
assert subject.is_pset_empty(pset) is False
|
||||
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Foo": None})
|
||||
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