mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 02:07:36 +00:00
Add target-unit pickers to the Qto panels; keep unit symbols live
This commit is contained in:
@@ -238,7 +238,6 @@ def import_attribute(
|
|||||||
elif data_type == "float":
|
elif data_type == "float":
|
||||||
measure_class = attribute.type_of_attribute().declared_type().name()
|
measure_class = attribute.type_of_attribute().declared_type().name()
|
||||||
new.special_type = tool.Pset.get_special_type_for_measure_class(measure_class)
|
new.special_type = tool.Pset.get_special_type_for_measure_class(measure_class)
|
||||||
new.unit_symbol = tool.Pset.get_unit_symbol_for_special_type(new.special_type, tool.Ifc.get())
|
|
||||||
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()
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
with Profiler("Quantify function time:"):
|
with Profiler("Quantify function time:"):
|
||||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
ifc5d.qto.edit_qtos(ifc_file, results, target_units=tool.Qto.get_target_units(), rules=rules)
|
||||||
|
|
||||||
not_quantified_elements = elements - set(results.keys())
|
not_quantified_elements = elements - set(results.keys())
|
||||||
not_quantified_message = tool.Qto.get_not_quantified_elements_message(not_quantified_elements)
|
not_quantified_message = tool.Qto.get_not_quantified_elements_message(not_quantified_elements)
|
||||||
@@ -194,7 +194,7 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
with Profiler("Quantify function time:"):
|
with Profiler("Quantify function time:"):
|
||||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
ifc5d.qto.edit_qtos(ifc_file, results, target_units=tool.Qto.get_target_units(), rules=rules)
|
||||||
not_quantified_elements = elements - set(results.keys())
|
not_quantified_elements = elements - set(results.keys())
|
||||||
return not_quantified_elements
|
return not_quantified_elements
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,28 @@ from bpy.props import (
|
|||||||
)
|
)
|
||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
|
|
||||||
|
import bonsai.bim.prop
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
|
||||||
CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = []
|
CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = []
|
||||||
|
|
||||||
|
# Measure class (matching ifc5d.qto's Function.measure / SI2ProjectUnitConverter.project_units'
|
||||||
|
# keys) -> (BIMQtoProperties field name, tool.Pset special_type, UI label).
|
||||||
|
MEASURE_TO_TARGET_UNIT_FIELD: dict[str, tuple[str, str, str]] = {
|
||||||
|
"IfcLengthMeasure": ("target_unit_length", "LENGTH", "Length"),
|
||||||
|
"IfcAreaMeasure": ("target_unit_area", "AREA", "Area"),
|
||||||
|
"IfcVolumeMeasure": ("target_unit_volume", "VOLUME", "Volume"),
|
||||||
|
"IfcMassMeasure": ("target_unit_mass", "MASS", "Mass"),
|
||||||
|
"IfcTimeMeasure": ("target_unit_time", "TIME", "Time"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _target_unit_items(special_type: str):
|
||||||
|
def getter(self: "BIMQtoProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||||
|
return bonsai.bim.prop.get_unit_enum_items_for_special_type(special_type, tool.Ifc.get())
|
||||||
|
|
||||||
|
return getter
|
||||||
|
|
||||||
|
|
||||||
def get_qto_rule(self: "BIMQtoProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
def get_qto_rule(self: "BIMQtoProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||||
results: list[tuple[str, str, str]] = []
|
results: list[tuple[str, str, str]] = []
|
||||||
@@ -92,6 +110,11 @@ class BIMQtoProperties(PropertyGroup):
|
|||||||
),
|
),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
target_unit_length: EnumProperty(items=_target_unit_items("LENGTH"), name="Length Unit")
|
||||||
|
target_unit_area: EnumProperty(items=_target_unit_items("AREA"), name="Area Unit")
|
||||||
|
target_unit_volume: EnumProperty(items=_target_unit_items("VOLUME"), name="Volume Unit")
|
||||||
|
target_unit_mass: EnumProperty(items=_target_unit_items("MASS"), name="Mass Unit")
|
||||||
|
target_unit_time: EnumProperty(items=_target_unit_items("TIME"), name="Time Unit")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
qto_rule: str
|
qto_rule: str
|
||||||
@@ -101,3 +124,8 @@ class BIMQtoProperties(PropertyGroup):
|
|||||||
qto_name: str
|
qto_name: str
|
||||||
prop_name: str
|
prop_name: str
|
||||||
fallback: bool
|
fallback: bool
|
||||||
|
target_unit_length: str
|
||||||
|
target_unit_area: str
|
||||||
|
target_unit_volume: str
|
||||||
|
target_unit_mass: str
|
||||||
|
target_unit_time: str
|
||||||
|
|||||||
@@ -17,9 +17,12 @@
|
|||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import ifc5d.qto
|
||||||
|
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
from bonsai.bim.helper import prop_with_search
|
||||||
from bonsai.bim.module.qto.data import QtoData
|
from bonsai.bim.module.qto.data import QtoData
|
||||||
|
from bonsai.bim.module.qto.prop import MEASURE_TO_TARGET_UNIT_FIELD
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_qto(bpy.types.Panel):
|
class BIM_PT_qto(bpy.types.Panel):
|
||||||
@@ -44,6 +47,14 @@ class BIM_PT_qto(bpy.types.Panel):
|
|||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.prop(props, "qto_rule", text="")
|
row.prop(props, "qto_rule", text="")
|
||||||
row.prop(props, "fallback", text="", icon="RADIOBUT_ON" if props.fallback else "RADIOBUT_OFF")
|
row.prop(props, "fallback", text="", icon="RADIOBUT_ON" if props.fallback else "RADIOBUT_OFF")
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Target Units (optional, otherwise project default)")
|
||||||
|
for field_name, _special_type, label in MEASURE_TO_TARGET_UNIT_FIELD.values():
|
||||||
|
row = box.row(align=True)
|
||||||
|
row.label(text=label)
|
||||||
|
prop_with_search(row, props, field_name, text="")
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.operator("bim.perform_quantity_take_off")
|
row.operator("bim.perform_quantity_take_off")
|
||||||
|
|
||||||
@@ -66,6 +77,15 @@ class BIM_PT_qto_manual(bpy.types.Panel):
|
|||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.prop(props, "calculator_function", text="Function")
|
row.prop(props, "calculator_function", text="Function")
|
||||||
|
|
||||||
|
calculator = ifc5d.qto.calculators.get(props.calculator)
|
||||||
|
function = calculator.functions.get(props.calculator_function) if calculator else None
|
||||||
|
target_unit_field = MEASURE_TO_TARGET_UNIT_FIELD.get(function.measure) if function else None
|
||||||
|
if target_unit_field:
|
||||||
|
field_name, _special_type, label = target_unit_field
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.label(text=f"{label} Unit")
|
||||||
|
prop_with_search(row, props, field_name, text="")
|
||||||
|
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.prop(props, "qto_name", text="")
|
row.prop(props, "qto_name", text="")
|
||||||
row.prop(props, "prop_name", text="")
|
row.prop(props, "prop_name", text="")
|
||||||
|
|||||||
@@ -123,32 +123,39 @@ def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) ->
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def get_attribute_unit_enum_items(prop: "Attribute", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
def get_unit_enum_items_for_special_type(
|
||||||
"""Items for `Attribute.unit_id_enum`: "(Project Default)" plus every candidate unit
|
special_type: str, ifc_file: Union[ifcopenshell.file, None]
|
||||||
matching `prop.special_type`, filtered per-instance since candidates depend on the
|
) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||||
attribute's own measure type (unlike the globally-shared lists in `bonsai.bim.ui.EnumData`).
|
"""Items for a unit-override picker: "Default (<symbol>)" plus every candidate unit
|
||||||
|
matching `special_type`, filtered per-caller since candidates depend on the measure type
|
||||||
|
in question (unlike the globally-shared lists in `bonsai.bim.ui.EnumData`).
|
||||||
"""
|
"""
|
||||||
ifc_file = tool.Ifc.get()
|
if not ifc_file or not tool.Pset.is_measurable_special_type(special_type):
|
||||||
if not ifc_file or not tool.Pset.is_measurable_special_type(prop.special_type):
|
|
||||||
return [(cache_string("0"), cache_string("Default"), "")]
|
return [(cache_string("0"), cache_string("Default"), "")]
|
||||||
|
|
||||||
default_symbol = tool.Pset.get_unit_symbol_for_special_type(prop.special_type, ifc_file)
|
default_symbol = tool.Pset.get_unit_symbol_for_special_type(special_type, ifc_file)
|
||||||
items: list[tuple[str, str, str]] = [
|
items: list[tuple[str, str, str]] = [
|
||||||
(cache_string("0"), cache_string(f"Default ({default_symbol})" if default_symbol else "Default"), "")
|
(cache_string("0"), cache_string(f"Default ({default_symbol})" if default_symbol else "Default"), "")
|
||||||
]
|
]
|
||||||
seen_ids = {0}
|
for unit in tool.Pset.get_candidate_units_for_special_type(special_type, ifc_file):
|
||||||
for unit in tool.Pset.get_candidate_units_for_special_type(prop.special_type, ifc_file):
|
|
||||||
name = getattr(unit, "Name", None) or unit.is_a()
|
name = getattr(unit, "Name", None) or unit.is_a()
|
||||||
symbol = ifcopenshell.util.unit.get_unit_symbol(unit)
|
symbol = ifcopenshell.util.unit.get_unit_symbol(unit)
|
||||||
label = f"{name} ({symbol})" if symbol else name
|
label = f"{name} ({symbol})" if symbol else name
|
||||||
items.append((cache_string(str(unit.id())), cache_string(label), ""))
|
items.append((cache_string(str(unit.id())), cache_string(label), ""))
|
||||||
seen_ids.add(unit.id())
|
return items
|
||||||
|
|
||||||
# Defensive: real-world files sometimes carry a Unit that doesn't cleanly match our
|
|
||||||
# candidate-matching logic (e.g. a mismatched UnitType). Always keep the attribute's own
|
def get_attribute_unit_enum_items(prop: "Attribute", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||||
# current override selectable/representable, however unusual, so setting unit_id_enum to
|
"""Items for `Attribute.unit_id_enum`. Wraps `get_unit_enum_items_for_special_type` with a
|
||||||
# match an already-seeded unit_id can never raise "enum not found".
|
defensive addition: real-world files sometimes carry a Unit that doesn't cleanly match our
|
||||||
if prop.unit_id and prop.unit_id not in seen_ids:
|
candidate-matching logic (e.g. a mismatched UnitType). Always keep the attribute's own
|
||||||
|
current override selectable/representable, however unusual, so setting unit_id_enum to
|
||||||
|
match an already-seeded unit_id can never raise "enum not found".
|
||||||
|
"""
|
||||||
|
ifc_file = tool.Ifc.get()
|
||||||
|
items = get_unit_enum_items_for_special_type(prop.special_type, ifc_file)
|
||||||
|
|
||||||
|
if prop.unit_id and prop.unit_id not in {int(i[0]) for i in items}:
|
||||||
own_unit = ifc_file.by_id(prop.unit_id)
|
own_unit = ifc_file.by_id(prop.unit_id)
|
||||||
name = getattr(own_unit, "Name", None) or own_unit.is_a()
|
name = getattr(own_unit, "Name", None) or own_unit.is_a()
|
||||||
symbol = ifcopenshell.util.unit.get_unit_symbol(own_unit)
|
symbol = ifcopenshell.util.unit.get_unit_symbol(own_unit)
|
||||||
@@ -302,6 +309,20 @@ def get_display_name(self: "Attribute") -> str:
|
|||||||
return f"{name}, {self.unit_symbol}"
|
return f"{name}, {self.unit_symbol}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_unit_symbol(self: "Attribute") -> str:
|
||||||
|
"""The symbol for whatever unit the value is currently expressed in: this property's own
|
||||||
|
override (`unit_id`) if set, else the project default for `special_type`. Computed fresh on
|
||||||
|
every access (rather than cached at import time) so it stays correct immediately after the
|
||||||
|
unit picker changes `unit_id`, and after the project's own default units are edited.
|
||||||
|
"""
|
||||||
|
if not tool.Pset.is_measurable_special_type(self.special_type):
|
||||||
|
return ""
|
||||||
|
if not (ifc_file := tool.Ifc.get()):
|
||||||
|
return ""
|
||||||
|
unit = tool.Pset.resolve_effective_unit(self.special_type, self.unit_id, ifc_file)
|
||||||
|
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
|
||||||
|
|
||||||
|
|
||||||
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
|
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
|
||||||
# Either "", "DATE", "DATETIME", "LOGICAL", "URI", "DURATION", or an
|
# Either "", "DATE", "DATETIME", "LOGICAL", "URI", "DURATION", or an
|
||||||
# IfcUnitEnum/IfcDerivedUnitEnum value with the "UNIT" suffix stripped (e.g.
|
# IfcUnitEnum/IfcDerivedUnitEnum value with the "UNIT" suffix stripped (e.g.
|
||||||
@@ -359,7 +380,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="")
|
unit_symbol: StringProperty(name="Unit Symbol", get=get_unit_symbol)
|
||||||
unit_id: IntProperty(name="Unit Override", default=0)
|
unit_id: IntProperty(name="Unit Override", default=0)
|
||||||
"""STEP id of this property/quantity's own Unit override. 0 means "use the project default"."""
|
"""STEP id of this property/quantity's own Unit override. 0 means "use the project default"."""
|
||||||
unit_id_enum: EnumProperty(items=get_attribute_unit_enum_items, name="Unit", update=update_attribute_unit_id)
|
unit_id_enum: EnumProperty(items=get_attribute_unit_enum_items, name="Unit", update=update_attribute_unit_id)
|
||||||
|
|||||||
@@ -398,12 +398,12 @@ 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())
|
# The prop's OWN Unit override only -- metadata.unit_symbol is computed fresh from
|
||||||
# The prop's OWN Unit override only, not the resolved project-default fallback
|
# special_type/unit_id on every access (see Attribute.get_unit_symbol), so it
|
||||||
# get_unit_symbol_for_prop() above already accounted for. Some real-world files
|
# already accounts for the project-default fallback once unit_id is set below.
|
||||||
# (e.g. certain exporters) set Unit on properties that aren't actually measures --
|
# Some real-world files (e.g. certain exporters) set Unit on properties that
|
||||||
# ignore it there, since we only ever treat Unit as meaningful for measurable
|
# aren't actually measures -- ignore it there, since we only ever treat Unit as
|
||||||
# special_types (matching the UI picker's own gating).
|
# meaningful for measurable special_types (matching the UI picker's own gating).
|
||||||
own_unit = getattr(prop, "Unit", None) if cls.is_measurable_special_type(metadata.special_type) else None
|
own_unit = getattr(prop, "Unit", None) if cls.is_measurable_special_type(metadata.special_type) else None
|
||||||
metadata.unit_id = own_unit.id() if own_unit else 0
|
metadata.unit_id = own_unit.id() if own_unit else 0
|
||||||
metadata.unit_id_enum = str(metadata.unit_id)
|
metadata.unit_id_enum = str(metadata.unit_id)
|
||||||
@@ -495,7 +495,6 @@ 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])
|
||||||
@@ -608,7 +607,6 @@ 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
|
||||||
|
|||||||
@@ -176,6 +176,19 @@ class Qto(bonsai.core.tool.Qto):
|
|||||||
is_ifc4x3 = ifc_file.schema == "IFC4X3"
|
is_ifc4x3 = ifc_file.schema == "IFC4X3"
|
||||||
return {rule_id: rule for rule_id, rule in ifc5d.qto.rules.items() if rule_id.startswith("IFC4X3") == is_ifc4x3}
|
return {rule_id: rule for rule_id, rule in ifc5d.qto.rules.items() if rule_id.startswith("IFC4X3") == is_ifc4x3}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_target_units(cls) -> dict[str, ifcopenshell.entity_instance]:
|
||||||
|
from bonsai.bim.module.qto.prop import MEASURE_TO_TARGET_UNIT_FIELD
|
||||||
|
|
||||||
|
props = cls.get_qto_props()
|
||||||
|
ifc_file = tool.Ifc.get()
|
||||||
|
target_units: dict[str, ifcopenshell.entity_instance] = {}
|
||||||
|
for measure_class, (field_name, _special_type, _label) in MEASURE_TO_TARGET_UNIT_FIELD.items():
|
||||||
|
unit_id = int(tool.Blender.get_enum_safe(props, field_name) or "0")
|
||||||
|
if unit_id:
|
||||||
|
target_units[measure_class] = ifc_file.by_id(unit_id)
|
||||||
|
return target_units
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_not_quantified_elements_message(cls, not_quantified_elements: set[ifcopenshell.entity_instance]) -> str:
|
def get_not_quantified_elements_message(cls, not_quantified_elements: set[ifcopenshell.entity_instance]) -> str:
|
||||||
not_quantified_message = ""
|
not_quantified_message = ""
|
||||||
|
|||||||
@@ -131,6 +131,71 @@ class TestGetAttributeUnitEnumItems(NewFile):
|
|||||||
assert [i[0] for i in items] == ["0"]
|
assert [i[0] for i in items] == ["0"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetUnitEnumItemsForSpecialType(NewFile):
|
||||||
|
def test_matches_the_attribute_wrapper_output(self):
|
||||||
|
# Regression test for extracting get_unit_enum_items_for_special_type out of
|
||||||
|
# get_attribute_unit_enum_items: the wrapper must still produce identical items for
|
||||||
|
# the plain (no own-unit-fallback-needed) case.
|
||||||
|
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])
|
||||||
|
ifcopenshell.api.unit.add_si_unit(ifc, unit_type="LENGTHUNIT")
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
prop = ifc.createIfcPropertySingleValue(Name="Foo", NominalValue=ifc.createIfcLengthMeasure(2.5))
|
||||||
|
metadata = import_single_property(ifc, element, prop)
|
||||||
|
|
||||||
|
direct_items = bonsai.bim.prop.get_unit_enum_items_for_special_type(metadata.special_type, ifc)
|
||||||
|
wrapper_items = bonsai.bim.prop.get_attribute_unit_enum_items(metadata, bpy.context)
|
||||||
|
assert direct_items == wrapper_items
|
||||||
|
|
||||||
|
def test_returns_just_default_when_ifc_file_is_none(self):
|
||||||
|
assert bonsai.bim.prop.get_unit_enum_items_for_special_type("LENGTH", None) == [("0", "Default", "")]
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnitSymbolWithAreaVolumeDerivedFromLength(NewFile):
|
||||||
|
"""Regression test: AREAUNIT/VOLUMEUNIT have no IfcDerivedUnitEnum member, so a project
|
||||||
|
whose area/volume default is an IfcDerivedUnit rather than a literal-UnitType-matching
|
||||||
|
IfcSIUnit/IfcConversionBasedUnit has no literal UnitType to match on.
|
||||||
|
ifcopenshell.util.unit.get_project_unit() used to only match by literal UnitType, so the
|
||||||
|
read-only unit symbol and the edit-mode "Default (<symbol>)" picker entry both silently
|
||||||
|
fell back to no symbol at all in that case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup_project_with_derived_area_and_volume(self, ifc):
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||||
|
length = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="LENGTHUNIT")
|
||||||
|
area = ifcopenshell.api.unit.add_derived_unit(ifc, "USERDEFINED", "area-ish", {length: 2})
|
||||||
|
volume = ifcopenshell.api.unit.add_derived_unit(ifc, "USERDEFINED", "volume-ish", {length: 3})
|
||||||
|
ifcopenshell.api.unit.assign_unit(ifc, units=[length, area, volume])
|
||||||
|
|
||||||
|
def test_default_picker_entry_shows_the_resolved_symbol(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
self.setup_project_with_derived_area_and_volume(ifc)
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
prop = ifc.createIfcPropertySingleValue(Name="Foo", NominalValue=ifc.createIfcAreaMeasure(5.0))
|
||||||
|
metadata = import_single_property(ifc, element, prop)
|
||||||
|
|
||||||
|
items = bonsai.bim.prop.get_attribute_unit_enum_items(metadata, bpy.context)
|
||||||
|
assert items[0][1] == "Default (m2)"
|
||||||
|
|
||||||
|
def test_read_only_display_resolves_the_symbol(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
self.setup_project_with_derived_area_and_volume(ifc)
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
prop = ifc.createIfcPropertySingleValue(Name="Foo", NominalValue=ifc.createIfcVolumeMeasure(5.0))
|
||||||
|
metadata = import_single_property(ifc, element, prop)
|
||||||
|
|
||||||
|
assert metadata.unit_symbol == "m3"
|
||||||
|
assert metadata.display_name == "Foo, m3"
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateAttributeUnitId(NewFile):
|
class TestUpdateAttributeUnitId(NewFile):
|
||||||
def test_syncs_unit_id_and_converts_float_value_when_unit_id_enum_changes(self):
|
def test_syncs_unit_id_and_converts_float_value_when_unit_id_enum_changes(self):
|
||||||
ifc = ifcopenshell.file()
|
ifc = ifcopenshell.file()
|
||||||
@@ -145,11 +210,42 @@ class TestUpdateAttributeUnitId(NewFile):
|
|||||||
metadata = import_single_property(ifc, element, prop)
|
metadata = import_single_property(ifc, element, prop)
|
||||||
assert metadata.unit_id == 0
|
assert metadata.unit_id == 0
|
||||||
assert metadata.float_value == 2500.0
|
assert metadata.float_value == 2500.0
|
||||||
|
assert metadata.unit_symbol == "mm"
|
||||||
|
assert metadata.display_name == "Foo, mm"
|
||||||
|
|
||||||
metadata.unit_id_enum = str(length_m.id())
|
metadata.unit_id_enum = str(length_m.id())
|
||||||
|
|
||||||
assert metadata.unit_id == length_m.id()
|
assert metadata.unit_id == length_m.id()
|
||||||
assert metadata.float_value == pytest.approx(2.5) # converted, not just relabeled
|
assert metadata.float_value == pytest.approx(2.5) # converted, not just relabeled
|
||||||
|
# Regression test: unit_symbol/display_name used to be a snapshot taken once at import
|
||||||
|
# time, so picking a different unit converted the value but left the label showing the
|
||||||
|
# old unit's symbol.
|
||||||
|
assert metadata.unit_symbol == "m"
|
||||||
|
assert metadata.display_name == "Foo, m"
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnitSymbolReflectsLiveProjectState(NewFile):
|
||||||
|
def test_symbol_updates_after_a_project_default_unit_is_assigned_later(self):
|
||||||
|
# Regression test: unit_symbol used to be a snapshot computed once at import time, so a
|
||||||
|
# property/quantity imported before its measure type had a project default unit assigned
|
||||||
|
# kept showing no symbol even after one was added, unless the panel was closed and
|
||||||
|
# reopened (re-triggering import). It's now computed fresh on every access.
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||||
|
# No AREAUNIT assigned yet.
|
||||||
|
|
||||||
|
element = ifc.createIfcWall()
|
||||||
|
prop = ifc.createIfcPropertySingleValue(Name="Foo", NominalValue=ifc.createIfcAreaMeasure(5.0))
|
||||||
|
metadata = import_single_property(ifc, element, prop)
|
||||||
|
assert metadata.unit_symbol == ""
|
||||||
|
assert metadata.display_name == "Foo"
|
||||||
|
|
||||||
|
area = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="AREAUNIT")
|
||||||
|
ifcopenshell.api.unit.assign_unit(ifc, units=[area])
|
||||||
|
|
||||||
|
assert metadata.unit_symbol == "m2"
|
||||||
|
assert metadata.display_name == "Foo, m2"
|
||||||
|
|
||||||
|
|
||||||
class TestImportPsetFromExistingWithAGenericNumericValueAndAnExplicitUnit(NewFile):
|
class TestImportPsetFromExistingWithAGenericNumericValueAndAnExplicitUnit(NewFile):
|
||||||
|
|||||||
@@ -181,6 +181,42 @@ class TestGetCalculatedObjectQuantities(test.bim.bootstrap.NewFile):
|
|||||||
assert quantities["NetVolume"] == 282.517
|
assert quantities["NetVolume"] == 282.517
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetTargetUnits(test.bim.bootstrap.NewFile):
|
||||||
|
def setup_file(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject", name="Test")
|
||||||
|
return ifc
|
||||||
|
|
||||||
|
def test_default_scene_state_returns_nothing(self):
|
||||||
|
self.setup_file()
|
||||||
|
assert subject.get_target_units() == {}
|
||||||
|
|
||||||
|
def test_setting_a_field_maps_it_to_its_measure_class(self):
|
||||||
|
ifc = self.setup_file()
|
||||||
|
metre = ifc.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")
|
||||||
|
millimetre = ifc.createIfcSIUnit(None, "LENGTHUNIT", "MILLI", "METRE")
|
||||||
|
ifcopenshell.api.unit.assign_unit(ifc, units=[metre])
|
||||||
|
|
||||||
|
props = tool.Qto.get_qto_props()
|
||||||
|
props.target_unit_length = str(millimetre.id())
|
||||||
|
|
||||||
|
assert subject.get_target_units() == {"IfcLengthMeasure": millimetre}
|
||||||
|
|
||||||
|
def test_untouched_fields_are_excluded(self):
|
||||||
|
ifc = self.setup_file()
|
||||||
|
metre = ifc.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")
|
||||||
|
millimetre = ifc.createIfcSIUnit(None, "LENGTHUNIT", "MILLI", "METRE")
|
||||||
|
gram = ifc.createIfcSIUnit(None, "MASSUNIT", None, "GRAM")
|
||||||
|
ifcopenshell.api.unit.assign_unit(ifc, units=[metre, gram])
|
||||||
|
|
||||||
|
props = tool.Qto.get_qto_props()
|
||||||
|
props.target_unit_length = str(millimetre.id())
|
||||||
|
props.target_unit_mass = "0" # explicitly left at "Default"
|
||||||
|
|
||||||
|
assert subject.get_target_units() == {"IfcLengthMeasure": millimetre}
|
||||||
|
|
||||||
|
|
||||||
class TestGetBaseQto(test.bim.bootstrap.NewFile):
|
class TestGetBaseQto(test.bim.bootstrap.NewFile):
|
||||||
def test_run(self):
|
def test_run(self):
|
||||||
ifc = ifcopenshell.file()
|
ifc = ifcopenshell.file()
|
||||||
|
|||||||
Reference in New Issue
Block a user