mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 19:07:57 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
@@ -889,6 +889,48 @@ class IfcImporter:
|
||||
obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element))
|
||||
)
|
||||
|
||||
if element.is_a("IfcAnnotation") and getattr(element, "ObjectType", None) == "IMAGE":
|
||||
image = None
|
||||
if obj.data and obj.data.materials and obj.data.materials[0]:
|
||||
material = obj.data.materials[0]
|
||||
if material.use_nodes and material.node_tree:
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type == "TEX_IMAGE" and node.image:
|
||||
image = node.image
|
||||
break
|
||||
if image:
|
||||
import bmesh
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
if not bm.loops.layers.uv:
|
||||
uv_layer = bm.loops.layers.uv.new()
|
||||
else:
|
||||
uv_layer = bm.loops.layers.uv.active
|
||||
|
||||
if bm.verts:
|
||||
min_x = min(v.co.x for v in bm.verts)
|
||||
max_x = max(v.co.x for v in bm.verts)
|
||||
min_y = min(v.co.y for v in bm.verts)
|
||||
max_y = max(v.co.y for v in bm.verts)
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
for face in bm.faces:
|
||||
for loop in face.loops:
|
||||
vert = loop.vert
|
||||
u = (vert.co.x - min_x) / width if width > 0 else 0.5
|
||||
v = (vert.co.y - min_y) / height if height > 0 else 0.5
|
||||
|
||||
u = max(0.0, min(1.0, u))
|
||||
v = max(0.0, min(1.0, v))
|
||||
|
||||
loop[uv_layer].uv = (u, v)
|
||||
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
obj.data.update()
|
||||
return obj
|
||||
|
||||
def load_existing_meshes(self) -> None:
|
||||
|
||||
@@ -58,7 +58,7 @@ def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes)
|
||||
|
||||
|
||||
class BIM_PT_object_attributes(Panel):
|
||||
bl_label = "Attributes"
|
||||
bl_label = "Object Attributes"
|
||||
bl_idname = "BIM_PT_object_attributes"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
|
||||
@@ -104,6 +104,7 @@ class ReferencesData:
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
data["ifcClassificationReference"] = reference
|
||||
del data["ReferencedSource"]
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
@@ -257,7 +257,9 @@ class EnableEditingClassificationReference(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Classification.get_classification_reference_props()
|
||||
props.reference_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
|
||||
ifc_reference = tool.Ifc.get().by_id(self.reference)
|
||||
bonsai.bim.helper.import_attributes(ifc_reference, props.reference_attributes)
|
||||
props.classification_system_name = ifc_reference.ReferencedSource.Name or ""
|
||||
props.active_reference_id = self.reference
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -324,9 +326,13 @@ class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Classification.get_classification_reference_props()
|
||||
attributes = bonsai.bim.helper.export_attributes(props.reference_attributes)
|
||||
ifc_file = tool.Ifc.get()
|
||||
reference_entity = ifc_file.by_id(props.active_reference_id)
|
||||
referenced_source = reference_entity.ReferencedSource
|
||||
if props.classification_system_name:
|
||||
referenced_source.Name = props.classification_system_name
|
||||
ifcopenshell.api.classification.edit_reference(
|
||||
ifc_file,
|
||||
reference=ifc_file.by_id(props.active_reference_id),
|
||||
reference=reference_entity,
|
||||
attributes=attributes,
|
||||
)
|
||||
bpy.ops.bim.disable_editing_classification_reference()
|
||||
|
||||
@@ -95,6 +95,7 @@ class BIMClassificationReferenceProperties(PropertyGroup):
|
||||
classifications: EnumProperty(items=get_classifications, name="Classifications")
|
||||
reference_attributes: CollectionProperty(name="Reference Attributes", type=Attribute)
|
||||
active_reference_id: IntProperty(name="Active Reference Id")
|
||||
classification_system_name: StringProperty(name="Classification System Name")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_adding: bool
|
||||
|
||||
@@ -21,6 +21,8 @@ import bpy
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.classification.prop as classification_prop
|
||||
import ifcopenshell.util.classification
|
||||
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.module.classification.data import (
|
||||
ClassificationsData,
|
||||
@@ -152,7 +154,15 @@ class ReferenceUI:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No References")
|
||||
|
||||
for reference in self.data.data["references"]:
|
||||
def get_classification_name(reference):
|
||||
classification_entity = ifcopenshell.util.classification.get_classification(
|
||||
reference["ifcClassificationReference"]
|
||||
)
|
||||
return classification_entity.Name if classification_entity else ""
|
||||
|
||||
sorted_references = sorted(self.data.data["references"], key=get_classification_name)
|
||||
|
||||
for reference in sorted_references:
|
||||
if self.props.active_reference_id == reference["id"]:
|
||||
self.draw_editable_ui()
|
||||
else:
|
||||
@@ -248,10 +258,20 @@ class ReferenceUI:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_classification_reference", text="Save changes", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "classification_system_name", text="Classification System Name")
|
||||
|
||||
bonsai.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
|
||||
|
||||
def draw_reference_ui(self, reference: dict[str, Any]) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
classification_entity = ifcopenshell.util.classification.get_classification(
|
||||
reference["ifcClassificationReference"]
|
||||
)
|
||||
classification_name = classification_entity.Name if classification_entity else ""
|
||||
row.label(text=classification_name, icon="OUTLINER_COLLECTION")
|
||||
|
||||
if self.file.schema == "IFC2X3":
|
||||
name = reference["ItemReference"] or "No Identification"
|
||||
else:
|
||||
|
||||
@@ -3716,18 +3716,100 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
x_length: bpy.props.FloatProperty(
|
||||
name="X Length",
|
||||
description="Width of the reference image in project units",
|
||||
default=1.0,
|
||||
min=0.001,
|
||||
soft_min=0.01,
|
||||
precision=3,
|
||||
)
|
||||
y_length: bpy.props.FloatProperty(
|
||||
name="Y Length",
|
||||
description="Height of the reference image in project units",
|
||||
default=1.0,
|
||||
min=0.001,
|
||||
soft_min=0.01,
|
||||
precision=3,
|
||||
)
|
||||
|
||||
show_dimensions_dialog: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if Path(tool.Ifc.get_path()).is_file():
|
||||
layout.prop(self, "use_relative_path")
|
||||
|
||||
if getattr(self, "show_dimensions_dialog", False):
|
||||
if tool.Ifc.get():
|
||||
length_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT")
|
||||
if length_unit:
|
||||
unit_name = ifcopenshell.util.unit.get_full_unit_name(length_unit).lower()
|
||||
else:
|
||||
unit_name = "project units"
|
||||
layout.label(text=f"Set Reference Image Dimensions (in {unit_name}):")
|
||||
else:
|
||||
layout.label(text="Set Reference Image Dimensions (in project units):")
|
||||
layout.separator()
|
||||
layout.prop(self, "x_length")
|
||||
layout.prop(self, "y_length")
|
||||
else:
|
||||
self.use_relative_path = False
|
||||
layout.label(text="Save the .ifc file first ")
|
||||
layout.label(text="to use relative paths.")
|
||||
layout.prop(self, "override_existing_image")
|
||||
layout.prop(self, "use_existing_object_by_name")
|
||||
if Path(tool.Ifc.get_path()).is_file():
|
||||
layout.prop(self, "use_relative_path")
|
||||
else:
|
||||
self.use_relative_path = False
|
||||
layout.label(text="Save the .ifc file first ")
|
||||
layout.label(text="to use relative paths.")
|
||||
layout.prop(self, "override_existing_image")
|
||||
layout.prop(self, "use_existing_object_by_name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not getattr(self, "show_dimensions_dialog", False):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
else:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
if not getattr(self, "show_dimensions_dialog", False):
|
||||
abs_path = Path(self.filepath).absolute().resolve()
|
||||
if self.override_existing_image:
|
||||
params = {"check_existing": True, "force_reload": True}
|
||||
else:
|
||||
params = {"check_existing": False}
|
||||
|
||||
try:
|
||||
image = load_image(abs_path.name, str(abs_path.parent), **params)
|
||||
|
||||
image_width_px = image.size[0]
|
||||
image_height_px = image.size[1]
|
||||
aspect_ratio = image_width_px / image_height_px
|
||||
|
||||
if aspect_ratio >= 1.0:
|
||||
self.x_length = 1.0
|
||||
self.y_length = 1.0 / aspect_ratio
|
||||
else:
|
||||
self.x_length = aspect_ratio
|
||||
self.y_length = 1.0
|
||||
|
||||
bpy.data.images.remove(image)
|
||||
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to load image: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.show_dimensions_dialog = True
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
return self._execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
space = tool.Blender.get_view3d_space()
|
||||
if space.shading.color_type != "TEXTURE":
|
||||
space.shading.color_type = "TEXTURE"
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
'"Object Color" for Viewport Shading: Solid changed to "Texture" to see the reference image properly.',
|
||||
)
|
||||
|
||||
abs_path = Path(self.filepath).absolute().resolve()
|
||||
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -3736,13 +3818,39 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
params = {"check_existing": True, "force_reload": True}
|
||||
else:
|
||||
params = {"check_existing": False}
|
||||
image = load_image(abs_path.name, abs_path.parent, **params)
|
||||
image = load_image(abs_path.name, str(abs_path.parent), **params)
|
||||
|
||||
def bm_add_image_plane(mesh):
|
||||
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True)
|
||||
plane_scale = (Vector(image.size) / min(image.size)).to_3d()
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
plane_scale = Vector((self.x_length * unit_scale / 2.0, self.y_length * unit_scale / 2.0, 1.0))
|
||||
matrix = Matrix.LocRotScale(None, None, plane_scale)
|
||||
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
|
||||
|
||||
if not bm.loops.layers.uv:
|
||||
uv_layer = bm.loops.layers.uv.new()
|
||||
else:
|
||||
uv_layer = bm.loops.layers.uv.active
|
||||
|
||||
min_x = min(v.co.x for v in bm.verts)
|
||||
max_x = max(v.co.x for v in bm.verts)
|
||||
min_y = min(v.co.y for v in bm.verts)
|
||||
max_y = max(v.co.y for v in bm.verts)
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
for face in bm.faces:
|
||||
for loop in face.loops:
|
||||
vert = loop.vert
|
||||
u = (vert.co.x - min_x) / width if width > 0 else 0.5
|
||||
v = (vert.co.y - min_y) / height if height > 0 else 0.5
|
||||
|
||||
u = max(0.0, min(1.0, u))
|
||||
v = max(0.0, min(1.0, v))
|
||||
loop[uv_layer].uv = (u, v)
|
||||
|
||||
tool.Blender.apply_bmesh(mesh, bm)
|
||||
|
||||
if self.use_existing_object_by_name:
|
||||
@@ -3763,6 +3871,23 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
)
|
||||
tool.Blender.remove_data_block(temp_mesh)
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and isinstance(obj.data, bpy.types.Mesh):
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if representation and representation.Items:
|
||||
item_id = representation.Items[0].id()
|
||||
num_faces = len(obj.data.polygons)
|
||||
obj.data["ios_item_ids"] = [item_id] * num_faces
|
||||
tool.Blender.Attribute.fill_attribute(obj.data, "ios_item_ids", "FACE", "INT", [item_id] * num_faces)
|
||||
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcPolygonalFaceSet") and item.Coordinates:
|
||||
new_coords = []
|
||||
for vertex in obj.data.vertices:
|
||||
co = obj.matrix_world @ vertex.co
|
||||
new_coords.append([co.x, co.y, co.z])
|
||||
item.Coordinates.CoordList = new_coords
|
||||
|
||||
tool.Blender.set_active_object(obj)
|
||||
|
||||
material = bpy.data.materials.new(name=image_filepath.stem)
|
||||
@@ -3803,6 +3928,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
tool.Style.reload_material_from_ifc(material)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ConvertSVGToDXF(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_svg_to_dxf"
|
||||
|
||||
@@ -42,6 +42,7 @@ classes = (
|
||||
operator.ExportIFC,
|
||||
operator.FlipClippingPlane,
|
||||
operator.IFCFileHandlerOperator,
|
||||
operator.ImageScalingTool,
|
||||
operator.LinkIfc,
|
||||
operator.LoadLink,
|
||||
operator.LoadLinkedProject,
|
||||
|
||||
@@ -119,6 +119,8 @@ class NewProject(bpy.types.Operator):
|
||||
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
|
||||
bim_props.area_unit = "SQUARE_METRE"
|
||||
bim_props.volume_unit = "CUBIC_METRE"
|
||||
bim_props.mass_unit = "KILOGRAM"
|
||||
bim_props.time_unit = "SECOND"
|
||||
pprops.template_file = "IFC4 Demo Template.ifc"
|
||||
|
||||
if self.preset != "wizard":
|
||||
@@ -2894,3 +2896,201 @@ class ClearMeasurement(bpy.types.Operator):
|
||||
MeasureDecorator.uninstall()
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImageScalingTool(bpy.types.Operator, PolylineOperator):
|
||||
bl_idname = "bim.image_scaling_tool"
|
||||
bl_label = "Image Scaling Tool"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.type == "VIEW_3D"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
bpy.types.Operator.__init__(self, *args, **kwargs)
|
||||
PolylineOperator.__init__(self)
|
||||
self.input_options = ["DISTANCE"]
|
||||
self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options)
|
||||
self.selected_points = []
|
||||
self.target_object = None
|
||||
self.current_distance_value = ""
|
||||
self.is_typing_distance = False
|
||||
self.calculated_distance = 0.0
|
||||
|
||||
if tool.Ifc.get():
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
else:
|
||||
self.unit_scale = tool.Blender.get_unit_scale()
|
||||
|
||||
def modal(self, context, event):
|
||||
if not self.target_object or not context.active_object or context.active_object != self.target_object:
|
||||
self.report({"ERROR"}, "Image annotation was deselected. Tool cancelled.")
|
||||
return self.cancel_tool(context)
|
||||
|
||||
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
self.handle_lock_axis(context, event)
|
||||
|
||||
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
|
||||
self.handle_mouse_move(context, event)
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
self.handle_custom_instructions(context)
|
||||
self.handle_mouse_move(context, event)
|
||||
self.choose_axis(event, z=True)
|
||||
self.choose_plane(event)
|
||||
self.handle_snap_selection(context, event)
|
||||
|
||||
if event.type == "LEFTMOUSE" and event.value == "PRESS":
|
||||
if len(self.selected_points) < 2:
|
||||
snapped_point = self.snapping_points[0]
|
||||
point_3d = snapped_point["point"].copy()
|
||||
self.selected_points.append(point_3d)
|
||||
|
||||
if len(self.selected_points) == 2:
|
||||
self.calculate_distance()
|
||||
self.current_distance_value = f"{self.calculated_distance:.3f}"
|
||||
self.is_typing_distance = False
|
||||
self.input_ui.set_value("DISTANCE", self.calculated_distance)
|
||||
|
||||
elif len(self.selected_points) == 2:
|
||||
if event.type in {"RET", "NUMPAD_ENTER"} and event.value == "PRESS":
|
||||
return self.apply_scaling(context)
|
||||
|
||||
if event.unicode and event.unicode.isprintable() and event.value == "PRESS":
|
||||
if event.unicode.isdigit() or event.unicode == ".":
|
||||
if not self.is_typing_distance:
|
||||
self.current_distance_value = event.unicode
|
||||
self.is_typing_distance = True
|
||||
else:
|
||||
self.current_distance_value += event.unicode
|
||||
|
||||
distance_value = float(self.current_distance_value)
|
||||
self.input_ui.set_value("DISTANCE", distance_value)
|
||||
|
||||
elif event.type in {"BACK_SPACE", "DEL"} and event.value == "PRESS":
|
||||
if len(self.current_distance_value) > 0:
|
||||
self.current_distance_value = self.current_distance_value[:-1]
|
||||
distance_value = (
|
||||
float(self.current_distance_value) if self.current_distance_value else self.calculated_distance
|
||||
)
|
||||
self.input_ui.set_value("DISTANCE", distance_value)
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
result = self.handle_cancelation(context, event)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
active_obj = context.active_object
|
||||
self.target_object = active_obj
|
||||
super().invoke(context, event)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def cancel_tool(self, context):
|
||||
context.workspace.status_text_set(text=None)
|
||||
if hasattr(self, "tool_state"):
|
||||
self.tool_state.plane_method = None
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Blender.update_viewport()
|
||||
return {"CANCELLED"}
|
||||
|
||||
def handle_custom_instructions(self, context):
|
||||
if len(self.selected_points) == 0:
|
||||
instruction_text = "Click First Point on Image"
|
||||
elif len(self.selected_points) == 1:
|
||||
instruction_text = "Click Second Point on Image"
|
||||
elif len(self.selected_points) == 2:
|
||||
if self.is_typing_distance:
|
||||
instruction_text = f"Distance: {self.current_distance_value} - Press Enter to Apply"
|
||||
else:
|
||||
instruction_text = f"Measured: {self.calculated_distance:.3f} - Type New Distance or Press Enter"
|
||||
else:
|
||||
instruction_text = "Image Scaling Tool"
|
||||
|
||||
context.workspace.status_text_set(text=instruction_text)
|
||||
|
||||
def calculate_distance(self):
|
||||
if len(self.selected_points) == 2:
|
||||
point1 = self.selected_points[0]
|
||||
point2 = self.selected_points[1]
|
||||
distance_3d = (point2 - point1).length
|
||||
self.calculated_distance = distance_3d / self.unit_scale
|
||||
|
||||
def apply_scaling(self, context):
|
||||
if len(self.selected_points) != 2:
|
||||
self.report({"ERROR"}, "Two points must be selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
target_distance = float(self.current_distance_value)
|
||||
|
||||
if target_distance <= 0:
|
||||
self.report({"ERROR"}, "Distance must be positive")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.calculated_distance <= 0:
|
||||
self.report({"ERROR"}, "Selected points are too close together")
|
||||
return {"CANCELLED"}
|
||||
|
||||
scale_factor = target_distance / self.calculated_distance
|
||||
|
||||
if self.target_object:
|
||||
import bmesh
|
||||
|
||||
mesh = self.target_object.data
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
bmesh.ops.scale(bm, vec=(scale_factor, scale_factor, 1.0), verts=bm.verts)
|
||||
|
||||
if bm.loops.layers.uv:
|
||||
uv_layer = bm.loops.layers.uv.active
|
||||
|
||||
min_x = min(v.co.x for v in bm.verts)
|
||||
max_x = max(v.co.x for v in bm.verts)
|
||||
min_y = min(v.co.y for v in bm.verts)
|
||||
max_y = max(v.co.y for v in bm.verts)
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
for face in bm.faces:
|
||||
for loop in face.loops:
|
||||
vert = loop.vert
|
||||
u = (vert.co.x - min_x) / width if width > 0 else 0.5
|
||||
v = (vert.co.y - min_y) / height if height > 0 else 0.5
|
||||
|
||||
u = max(0.0, min(1.0, u))
|
||||
v = max(0.0, min(1.0, v))
|
||||
loop[uv_layer].uv = (u, v)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.update()
|
||||
|
||||
element = tool.Ifc.get_entity(self.target_object)
|
||||
if element and element.Representation:
|
||||
for representation in element.Representation.Representations:
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcPolygonalFaceSet") and item.Coordinates:
|
||||
new_coords = []
|
||||
for vertex in mesh.vertices:
|
||||
co = self.target_object.matrix_world @ vertex.co
|
||||
new_coords.append([co.x, co.y, co.z])
|
||||
|
||||
item.Coordinates.CoordList = new_coords
|
||||
|
||||
self.report({"INFO"}, f"Applied scale factor: {scale_factor:.4f}")
|
||||
|
||||
context.workspace.status_text_set(text=None)
|
||||
self.tool_state.plane_method = None
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -356,8 +356,23 @@ class BIM_PT_new_project_wizard(Panel):
|
||||
row.prop(props, "area_unit", text="Area Unit")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "volume_unit", text="Volume Unit")
|
||||
row = self.layout.row()
|
||||
prop_with_search(self.layout, pprops, "template_file", text="Template")
|
||||
|
||||
if tool.Blender.get_addon_preferences().mass_time_units_in_wizard:
|
||||
header, body = self.layout.panel("Mass and Time Units", default_closed=True)
|
||||
if header:
|
||||
header.label(text="Mass and Time Units")
|
||||
if body:
|
||||
label = "Add Mass and Time Units" if not props.add_mass_time_units else "Remove Mass and Time Units"
|
||||
body.prop(props, "add_mass_time_units", toggle=True, text=label)
|
||||
if props.add_mass_time_units:
|
||||
row = body.row()
|
||||
row.prop(props, "mass_unit", text="Mass Unit")
|
||||
row = body.row()
|
||||
row.prop(props, "time_unit", text="Time Unit")
|
||||
|
||||
self.layout.use_property_split = True
|
||||
row = self.layout.row()
|
||||
row.operator("bim.create_project")
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class ExploreTool(bpy.types.WorkSpaceTool):
|
||||
("bim.explore_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
|
||||
("bim.explore_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
|
||||
("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
|
||||
("bim.explore_hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, ws_tool):
|
||||
@@ -71,6 +72,14 @@ class ExploreTool(bpy.types.WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.clear_measurement", text="", icon="X")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_S")
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
|
||||
op.hotkey = "S_S"
|
||||
op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them"
|
||||
|
||||
|
||||
class ExploreHotkey(bpy.types.Operator):
|
||||
bl_idname = "bim.explore_hotkey"
|
||||
@@ -110,3 +119,20 @@ class ExploreHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.measure_face_area_tool("INVOKE_DEFAULT")
|
||||
else:
|
||||
bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type)
|
||||
|
||||
def hotkey_S_S(self):
|
||||
active_obj = bpy.context.active_object
|
||||
selected_objects = tool.Blender.get_selected_objects()
|
||||
element = tool.Ifc.get_entity(active_obj) if active_obj else None
|
||||
|
||||
if (
|
||||
not active_obj
|
||||
or not element
|
||||
or not element.is_a("IfcAnnotation")
|
||||
or len(selected_objects) != 1
|
||||
or not tool.Drawing.is_annotation_object_type(element, "IMAGE")
|
||||
):
|
||||
self.report({"ERROR"}, "Please select one image annotation first.")
|
||||
return
|
||||
|
||||
bpy.ops.bim.image_scaling_tool("INVOKE_DEFAULT")
|
||||
|
||||
@@ -23,8 +23,11 @@ classes = (
|
||||
operator.AssignType,
|
||||
operator.AutoRenameOccurrences,
|
||||
operator.DisableEditingType,
|
||||
operator.DisableEditingTypeAttributes,
|
||||
operator.DuplicateType,
|
||||
operator.EditTypeAttributes,
|
||||
operator.EnableEditingType,
|
||||
operator.EnableEditingTypeAttributes,
|
||||
operator.RemoveType,
|
||||
operator.RenameType,
|
||||
operator.SelectSimilarType,
|
||||
@@ -33,6 +36,7 @@ classes = (
|
||||
operator.UnassignType,
|
||||
prop.BIMTypeProperties,
|
||||
ui.BIM_PT_type,
|
||||
ui.BIM_PT_type_attributes,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class TypeData:
|
||||
"is_product": cls.is_product(),
|
||||
"total_instances": cls.total_instances(),
|
||||
"relating_type": cls.relating_type(),
|
||||
"relating_type_attributes": cls.relating_type_attributes(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -92,3 +93,25 @@ class TypeData:
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
return {"id": element_type.id(), "name": f"{element_type.is_a()}/{element_type.Name or 'Unnamed'}"}
|
||||
|
||||
@classmethod
|
||||
def relating_type_attributes(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if not element_type:
|
||||
return results
|
||||
|
||||
data = element_type.get_info()
|
||||
if "GlobalId" in data:
|
||||
excluded_keys = ["id", "type"]
|
||||
else:
|
||||
excluded_keys = ["type"]
|
||||
exclude_value_types = (tuple, ifcopenshell.entity_instance)
|
||||
for key, value in data.items():
|
||||
if value is None or isinstance(value, exclude_value_types) or key in excluded_keys:
|
||||
continue
|
||||
if key == "id":
|
||||
key = "STEP ID"
|
||||
results.append({"name": key, "value": str(value)})
|
||||
return results
|
||||
|
||||
@@ -24,6 +24,7 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.attribute
|
||||
import ifcopenshell.api.type
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
@@ -416,3 +417,80 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.layout.prop(
|
||||
self, "assign_selected_objects", text=f"Assign {len(ifc_objects)} Selected Object(s) to New Type"
|
||||
)
|
||||
|
||||
|
||||
class EnableEditingTypeAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_type_attributes"
|
||||
bl_label = "Enable Editing Type Attributes"
|
||||
bl_description = "Enable editing the attributes of the relating type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if not element_type:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Type.get_object_type_props(obj)
|
||||
props.type_attributes.clear()
|
||||
|
||||
bonsai.bim.helper.import_attributes(element_type, props.type_attributes)
|
||||
props.is_editing_type_attributes = True
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingTypeAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_type_attributes"
|
||||
bl_label = "Disable Editing Type Attributes"
|
||||
bl_description = "Disable editing the attributes of the relating type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Type.get_object_type_props(obj)
|
||||
props.type_attributes.clear()
|
||||
props.property_unset("is_editing_type_attributes")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditTypeAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_type_attributes"
|
||||
bl_label = "Edit Type Attributes"
|
||||
bl_description = "Save the changes to the relating type's attributes"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if not element_type:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Type.get_object_type_props(obj)
|
||||
attributes = bonsai.bim.helper.export_attributes(props.type_attributes)
|
||||
|
||||
ifcopenshell.api.attribute.edit_attributes(tool.Ifc.get(), product=element_type, attributes=attributes)
|
||||
|
||||
type_obj = tool.Ifc.get_object(element_type)
|
||||
if type_obj:
|
||||
tool.Root.set_object_name(type_obj, element_type)
|
||||
|
||||
bpy.ops.bim.disable_editing_type_attributes()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.type
|
||||
from bonsai.bim.module.type.data import TypeData
|
||||
from bonsai.bim.prop import Attribute
|
||||
import bonsai.tool as tool
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -90,9 +91,13 @@ class BIMTypeProperties(PropertyGroup):
|
||||
update=update_relating_type_from_object,
|
||||
poll=is_object_class_applicable,
|
||||
)
|
||||
is_editing_type_attributes: BoolProperty(name="Is Editing Type Attributes")
|
||||
type_attributes: CollectionProperty(type=Attribute, name="Type Attributes")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing_type: bool
|
||||
relating_type_class: str
|
||||
relating_type: str
|
||||
relating_type_object: Union[bpy.types.Object, None]
|
||||
is_editing_type_attributes: bool
|
||||
type_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
|
||||
@@ -20,7 +20,7 @@ import bpy
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.type.prop as type_prop
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.helper import prop_with_search, get_display_value
|
||||
from bonsai.bim.module.type.data import TypeData
|
||||
|
||||
|
||||
@@ -107,5 +107,50 @@ class BIM_PT_type(Panel):
|
||||
row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="")
|
||||
|
||||
|
||||
class BIM_PT_type_attributes(Panel):
|
||||
bl_label = "Type Attributes"
|
||||
bl_idname = "BIM_PT_type_attributes"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_type"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not TypeData.is_loaded:
|
||||
TypeData.load()
|
||||
return bool(TypeData.data.get("is_product") and TypeData.data.get("relating_type"))
|
||||
|
||||
def draw(self, context):
|
||||
if not TypeData.is_loaded:
|
||||
TypeData.load()
|
||||
|
||||
assert (layout := self.layout)
|
||||
assert (obj := context.active_object)
|
||||
|
||||
if not TypeData.data.get("relating_type"):
|
||||
layout.label(text="No Relating Type", icon="INFO")
|
||||
return
|
||||
|
||||
props = tool.Type.get_object_type_props(obj)
|
||||
|
||||
if props.is_editing_type_attributes:
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.edit_type_attributes", icon="CHECKMARK", text="Save Attributes")
|
||||
row.operator("bim.disable_editing_type_attributes", icon="CANCEL", text="")
|
||||
|
||||
import bonsai.bim.helper
|
||||
bonsai.bim.helper.draw_attributes(props.type_attributes, layout)
|
||||
else:
|
||||
row = layout.row()
|
||||
row.operator("bim.enable_editing_type_attributes", icon="GREASEPENCIL", text="Edit")
|
||||
|
||||
for attribute in TypeData.data["relating_type_attributes"]:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
value = get_display_value(attribute["value"])
|
||||
row.label(text=value)
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator("bim.add_occurrence", icon="PLUGIN")
|
||||
|
||||
@@ -586,7 +586,33 @@ class BIMProperties(PropertyGroup):
|
||||
],
|
||||
name="IFC Volume Unit",
|
||||
)
|
||||
add_mass_time_units: bpy.props.BoolProperty(
|
||||
name="Add Mass and Time Units",
|
||||
description="Enable to define mass and time units for the project",
|
||||
default=False,
|
||||
)
|
||||
mass_unit: EnumProperty(
|
||||
items=[
|
||||
("KILOGRAM", "Kilogram", "Kilograms"),
|
||||
("GRAM", "Gram", "Grams"),
|
||||
("POUND", "Pound", "Pounds"),
|
||||
("OUNCE", "Ounce", "Ounces"),
|
||||
("TONNE", "Tonne", "Metric Tons"),
|
||||
],
|
||||
name="Mass Unit",
|
||||
default="KILOGRAM",
|
||||
)
|
||||
|
||||
time_unit: EnumProperty(
|
||||
items=[
|
||||
("SECOND", "Second", "Seconds"),
|
||||
("MINUTE", "Minute", "Minutes"),
|
||||
("HOUR", "Hour", "Hours"),
|
||||
("DAY", "Day", "Days"),
|
||||
],
|
||||
name="Time Unit",
|
||||
default="HOUR",
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
is_dirty: bool
|
||||
schema_dir: str
|
||||
@@ -599,6 +625,8 @@ class BIMProperties(PropertyGroup):
|
||||
section_line_decorator_width: float
|
||||
area_unit: str
|
||||
volume_unit: str
|
||||
mass_unit: str
|
||||
time_unit: str
|
||||
|
||||
|
||||
class IfcParameter(PropertyGroup):
|
||||
|
||||
@@ -684,6 +684,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
default=False,
|
||||
)
|
||||
|
||||
mass_time_units_in_wizard: BoolProperty(
|
||||
name="Mass and time units in project wizard",
|
||||
description="Show mass and time units section in the new project wizard panel",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
svg2pdf_command: str
|
||||
svg2dxf_command: str
|
||||
@@ -720,6 +726,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
doc: DocPreferences
|
||||
default_parameters: DefaultParameters
|
||||
container_hide_show_isolate: bool
|
||||
mass_time_units_in_wizard: bool
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
@@ -901,6 +908,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
|
||||
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "container_hide_show_isolate")
|
||||
layout.prop(self, "mass_time_units_in_wizard")
|
||||
|
||||
|
||||
# Scene panel groups
|
||||
@@ -1355,7 +1363,7 @@ class BIM_PT_tab_sandbox(Panel):
|
||||
|
||||
# Object panel groups
|
||||
class BIM_PT_tab_object_metadata(Panel):
|
||||
bl_label = "Object Metadata"
|
||||
bl_label = "Object"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
@@ -1123,7 +1123,7 @@ class Unit:
|
||||
def set_active_unit(cls, unit): pass
|
||||
def get_project_currency_unit(cls): pass
|
||||
def get_currency_name(cls): pass
|
||||
|
||||
def add_mass_and_time_units(cls): pass
|
||||
|
||||
@interface
|
||||
class Voider:
|
||||
|
||||
@@ -28,20 +28,43 @@ if TYPE_CHECKING:
|
||||
|
||||
def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None:
|
||||
if unit.is_scene_unit_metric():
|
||||
prefix = unit.get_scene_unit_si_prefix("LENGTHUNIT")
|
||||
lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=prefix)
|
||||
prefix = unit.get_scene_unit_si_prefix("AREAUNIT")
|
||||
areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=prefix)
|
||||
prefix = unit.get_scene_unit_si_prefix("VOLUMEUNIT")
|
||||
volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=prefix)
|
||||
lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=unit.get_scene_unit_si_prefix("LENGTHUNIT"))
|
||||
areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=unit.get_scene_unit_si_prefix("AREAUNIT"))
|
||||
volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=unit.get_scene_unit_si_prefix("VOLUMEUNIT"))
|
||||
planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree")
|
||||
units = [lengthunit, areaunit, volumeunit, planeangleunit]
|
||||
|
||||
if unit.add_mass_and_time_units():
|
||||
prefix = unit.get_scene_unit_si_prefix("MASSUNIT")
|
||||
if prefix == "CONVERSION":
|
||||
massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower())
|
||||
else:
|
||||
massunit = ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix=prefix)
|
||||
prefix = unit.get_scene_unit_si_prefix("TIMEUNIT")
|
||||
if prefix == "CONVERSION":
|
||||
timeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("TIMEUNIT").lower())
|
||||
else:
|
||||
timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=prefix)
|
||||
units += [massunit, timeunit]
|
||||
|
||||
else:
|
||||
lengthunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("LENGTHUNIT"))
|
||||
areaunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("AREAUNIT"))
|
||||
volumeunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("VOLUMEUNIT"))
|
||||
planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree")
|
||||
units = [lengthunit, areaunit, volumeunit, planeangleunit]
|
||||
|
||||
planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree")
|
||||
|
||||
ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit, planeangleunit])
|
||||
if unit.add_mass_and_time_units():
|
||||
massunit = ifc.run("unit.add_conversion_based_unit", name=unit.get_scene_unit_name("MASSUNIT").lower())
|
||||
time_unit_name = unit.get_scene_unit_name("TIMEUNIT")
|
||||
if time_unit_name == "SECOND":
|
||||
timeunit = ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None)
|
||||
else:
|
||||
timeunit = ifc.run("unit.add_conversion_based_unit", name=time_unit_name.lower())
|
||||
units += [massunit, timeunit]
|
||||
print("Add mass and time units:", unit.add_mass_and_time_units())
|
||||
print("Assigning units:", units)
|
||||
ifc.run("unit.assign_unit", units=units)
|
||||
|
||||
|
||||
def assign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None:
|
||||
|
||||
@@ -266,7 +266,7 @@ def parse_distance_string(input_string: str, use_project_unit: bool = True) -> t
|
||||
|
||||
|
||||
class Unit(bonsai.core.tool.Unit):
|
||||
UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT"]
|
||||
UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT", "MASSUNIT", "TIMEUNIT"]
|
||||
|
||||
@staticmethod
|
||||
def format_distance(meters: float, use_imperial: bool = None, **kwargs) -> str:
|
||||
@@ -343,6 +343,10 @@ class Unit(bonsai.core.tool.Unit):
|
||||
return bim_props.area_unit
|
||||
elif unit_type == "VOLUMEUNIT":
|
||||
return bim_props.volume_unit
|
||||
elif unit_type == "MASSUNIT":
|
||||
return bim_props.mass_unit.lower()
|
||||
elif unit_type == "TIMEUNIT":
|
||||
return bim_props.time_unit.lower()
|
||||
else:
|
||||
assert_never(unit_type)
|
||||
|
||||
@@ -359,6 +363,24 @@ class Unit(bonsai.core.tool.Unit):
|
||||
unit = bim_props.area_unit
|
||||
elif unit_type == "VOLUMEUNIT":
|
||||
unit = bim_props.volume_unit
|
||||
elif unit_type == "MASSUNIT":
|
||||
unit = bim_props.mass_unit
|
||||
if unit == "GRAM":
|
||||
return None
|
||||
elif unit == "KILOGRAM":
|
||||
return "KILO"
|
||||
elif unit == "TONNE":
|
||||
return "MEGA"
|
||||
elif unit in ["POUND", "OUNCE"]:
|
||||
return "CONVERSION"
|
||||
else:
|
||||
return None
|
||||
elif unit_type == "TIMEUNIT":
|
||||
unit = bim_props.time_unit
|
||||
if unit == "SECOND":
|
||||
return None
|
||||
else:
|
||||
return "CONVERSION"
|
||||
else:
|
||||
assert_never(unit_type)
|
||||
if "/" in unit:
|
||||
@@ -477,3 +499,9 @@ class Unit(bonsai.core.tool.Unit):
|
||||
elif ifc_class == "IfcMonetaryUnit":
|
||||
return "COPY_ID"
|
||||
return "MOD_MESHDEFORM"
|
||||
|
||||
@classmethod
|
||||
def add_mass_and_time_units(cls) -> bool:
|
||||
"""Return True if the user wants to add mass and time units, False otherwise."""
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
return getattr(bim_props, "add_mass_time_units", False)
|
||||
|
||||
@@ -26,38 +26,119 @@ class TestAssignSceneUnits:
|
||||
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix")
|
||||
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix")
|
||||
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("KILO")
|
||||
unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return(None)
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return(
|
||||
"lengthunit"
|
||||
)
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit")
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return(
|
||||
"volumeunit"
|
||||
)
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="MASSUNIT", prefix="KILO").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_and_assigning_metric_units_without_mass_and_time(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("CENTI")
|
||||
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("CENTI")
|
||||
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("CENTI")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(False)
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="CENTI").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="CENTI").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="CENTI").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_and_assigning_imperial_units(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(False)
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("lengthname")
|
||||
ifc.run("unit.add_conversion_based_unit", name="lengthname").should_be_called().will_return("lengthunit")
|
||||
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("areaname")
|
||||
ifc.run("unit.add_conversion_based_unit", name="areaname").should_be_called().will_return("areaunit")
|
||||
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("volumename")
|
||||
ifc.run("unit.add_conversion_based_unit", name="volumename").should_be_called().will_return("volumeunit")
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("foot")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square foot")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic foot")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("pound")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("SECOND")
|
||||
|
||||
ifc.run("unit.add_conversion_based_unit", name="foot").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="square foot").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="cubic foot").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="pound").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="TIMEUNIT", prefix=None).should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_and_assigning_imperial_units_without_mass_and_time(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(False)
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("yard")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square yard")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic yard")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(False)
|
||||
ifc.run("unit.add_conversion_based_unit", name="yard").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="square yard").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="cubic yard").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
ifc.run("unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit"]).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
|
||||
def test_creating_metric_units_with_conversion_based_mass_and_time(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("MILLI")
|
||||
unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return(None)
|
||||
unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return(None)
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_si_prefix("MASSUNIT").should_be_called().will_return("CONVERSION")
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("tonne")
|
||||
unit.get_scene_unit_si_prefix("TIMEUNIT").should_be_called().will_return("CONVERSION")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("minute")
|
||||
|
||||
ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="MILLI").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=None).should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=None).should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="tonne").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="minute").should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
def test_creating_imperial_units_with_conversion_based_units(self, ifc, unit):
|
||||
unit.is_scene_unit_metric().should_be_called().will_return(False)
|
||||
unit.get_scene_unit_name("LENGTHUNIT").should_be_called().will_return("inch")
|
||||
unit.get_scene_unit_name("AREAUNIT").should_be_called().will_return("square inch")
|
||||
unit.get_scene_unit_name("VOLUMEUNIT").should_be_called().will_return("cubic inch")
|
||||
unit.add_mass_and_time_units().should_be_called().will_return(True)
|
||||
unit.get_scene_unit_name("MASSUNIT").should_be_called().will_return("ounce")
|
||||
unit.get_scene_unit_name("TIMEUNIT").should_be_called().will_return("hour")
|
||||
|
||||
ifc.run("unit.add_conversion_based_unit", name="inch").should_be_called().will_return("lengthunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="square inch").should_be_called().will_return("areaunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="cubic inch").should_be_called().will_return("volumeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="ounce").should_be_called().will_return("massunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="hour").should_be_called().will_return("timeunit")
|
||||
ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit")
|
||||
|
||||
ifc.run(
|
||||
"unit.assign_unit", units=["lengthunit", "areaunit", "volumeunit", "planeangleunit", "massunit", "timeunit"]
|
||||
).should_be_called()
|
||||
subject.assign_scene_units(ifc, unit)
|
||||
|
||||
|
||||
class TestAssignUnit:
|
||||
def test_run(self, ifc, unit):
|
||||
ifc.run("unit.assign_unit", units=["unit"]).should_be_called()
|
||||
|
||||
@@ -135,6 +135,35 @@ class TestGetSceneUnitName(NewFile):
|
||||
bpy.context.scene.unit_settings.system = "NONE"
|
||||
assert subject.get_scene_unit_name("LENGTHUNIT") == "foot"
|
||||
|
||||
def test_getting_mass_unit_names(self):
|
||||
"""Test getting mass unit names for different systems"""
|
||||
assert bpy.context.scene
|
||||
props = tool.Blender.get_bim_props()
|
||||
props.mass_unit = "GRAM"
|
||||
assert subject.get_scene_unit_name("MASSUNIT") == "gram"
|
||||
props.mass_unit = "KILOGRAM"
|
||||
assert subject.get_scene_unit_name("MASSUNIT") == "kilogram"
|
||||
props.mass_unit = "POUND"
|
||||
assert subject.get_scene_unit_name("MASSUNIT") == "pound"
|
||||
props.mass_unit = "OUNCE"
|
||||
assert subject.get_scene_unit_name("MASSUNIT") == "ounce"
|
||||
props.mass_unit = "TONNE"
|
||||
assert subject.get_scene_unit_name("MASSUNIT") == "tonne"
|
||||
|
||||
def test_getting_time_unit_names(self):
|
||||
"""Test getting time unit names for different systems"""
|
||||
assert bpy.context.scene
|
||||
props = tool.Blender.get_bim_props()
|
||||
|
||||
props.time_unit = "SECOND"
|
||||
assert subject.get_scene_unit_name("TIMEUNIT") == "second"
|
||||
props.time_unit = "MINUTE"
|
||||
assert subject.get_scene_unit_name("TIMEUNIT") == "minute"
|
||||
props.time_unit = "HOUR"
|
||||
assert subject.get_scene_unit_name("TIMEUNIT") == "hour"
|
||||
props.time_unit = "DAY"
|
||||
assert subject.get_scene_unit_name("TIMEUNIT") == "day"
|
||||
|
||||
|
||||
class TestGetSceneUnitSIPrefix:
|
||||
def test_run(self):
|
||||
@@ -162,6 +191,30 @@ class TestGetSceneUnitSIPrefix:
|
||||
props.volume_unit = "MILLI/CUBIC_METRE"
|
||||
assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") == "MILLI"
|
||||
|
||||
def test_mass_and_time_unit_prefixes(self):
|
||||
assert bpy.context.scene
|
||||
props = tool.Blender.get_bim_props()
|
||||
|
||||
props.mass_unit = "KILOGRAM"
|
||||
assert subject.get_scene_unit_si_prefix("MASSUNIT") == "KILO"
|
||||
props.mass_unit = "GRAM"
|
||||
assert subject.get_scene_unit_si_prefix("MASSUNIT") is None
|
||||
props.mass_unit = "POUND"
|
||||
assert subject.get_scene_unit_si_prefix("MASSUNIT") == "CONVERSION"
|
||||
props.mass_unit = "OUNCE"
|
||||
assert subject.get_scene_unit_si_prefix("MASSUNIT") == "CONVERSION"
|
||||
props.mass_unit = "TONNE"
|
||||
assert subject.get_scene_unit_si_prefix("MASSUNIT") == "MEGA"
|
||||
|
||||
props.time_unit = "SECOND"
|
||||
assert subject.get_scene_unit_si_prefix("TIMEUNIT") is None
|
||||
props.time_unit = "MINUTE"
|
||||
assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION"
|
||||
props.time_unit = "HOUR"
|
||||
assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION"
|
||||
props.time_unit = "DAY"
|
||||
assert subject.get_scene_unit_si_prefix("TIMEUNIT") == "CONVERSION"
|
||||
|
||||
|
||||
class TestImportUnitAttributes(NewFile):
|
||||
def test_importing_derived_units(self):
|
||||
@@ -298,6 +351,96 @@ class TestImportUnits(NewFile):
|
||||
assert props.units[5].unit_type == unit6.UnitType
|
||||
assert props.units[5].ifc_class == unit6.is_a()
|
||||
|
||||
def test_importing_mass_and_time_units(self):
|
||||
"""Test importing mass and time conversion based units"""
|
||||
ifc = ifcopenshell.api.project.create_file()
|
||||
tool.Ifc.set(ifc)
|
||||
|
||||
tonne_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="tonne")
|
||||
pound_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="pound")
|
||||
ounce_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="ounce")
|
||||
|
||||
minute_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="minute")
|
||||
hour_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="hour")
|
||||
day_unit = ifcopenshell.api.unit.add_conversion_based_unit(ifc, name="day")
|
||||
|
||||
kg_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="MASSUNIT", prefix="KILO")
|
||||
gram_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="MASSUNIT")
|
||||
second_unit = ifcopenshell.api.unit.add_si_unit(ifc, unit_type="TIMEUNIT")
|
||||
|
||||
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
|
||||
ifcopenshell.api.unit.assign_unit(ifc, units=[tonne_unit, minute_unit, kg_unit])
|
||||
|
||||
subject.import_units()
|
||||
props = tool.Unit.get_unit_props()
|
||||
|
||||
assert len(props.units) == 15
|
||||
|
||||
unit_ids = [u.ifc_definition_id for u in props.units]
|
||||
assert tonne_unit.id() in unit_ids
|
||||
assert pound_unit.id() in unit_ids
|
||||
assert ounce_unit.id() in unit_ids
|
||||
assert minute_unit.id() in unit_ids
|
||||
assert hour_unit.id() in unit_ids
|
||||
assert day_unit.id() in unit_ids
|
||||
assert kg_unit.id() in unit_ids
|
||||
assert gram_unit.id() in unit_ids
|
||||
assert second_unit.id() in unit_ids
|
||||
|
||||
tonne_prop = next(u for u in props.units if u.ifc_definition_id == tonne_unit.id())
|
||||
assert tonne_prop.name == "tonne"
|
||||
assert tonne_prop.unit_type == "MASSUNIT"
|
||||
assert tonne_prop.is_assigned is True
|
||||
assert tonne_prop.ifc_class == "IfcConversionBasedUnit"
|
||||
|
||||
pound_prop = next(u for u in props.units if u.ifc_definition_id == pound_unit.id())
|
||||
assert pound_prop.name == "pound"
|
||||
assert pound_prop.unit_type == "MASSUNIT"
|
||||
assert pound_prop.is_assigned is False
|
||||
assert pound_prop.ifc_class == "IfcConversionBasedUnit"
|
||||
|
||||
ounce_prop = next(u for u in props.units if u.ifc_definition_id == ounce_unit.id())
|
||||
assert ounce_prop.name == "ounce"
|
||||
assert ounce_prop.unit_type == "MASSUNIT"
|
||||
assert ounce_prop.is_assigned is False
|
||||
assert ounce_prop.ifc_class == "IfcConversionBasedUnit"
|
||||
|
||||
kg_prop = next(u for u in props.units if u.ifc_definition_id == kg_unit.id())
|
||||
assert kg_prop.name == "KILOGRAM"
|
||||
assert kg_prop.unit_type == "MASSUNIT"
|
||||
assert kg_prop.is_assigned is True
|
||||
assert kg_prop.ifc_class == "IfcSIUnit"
|
||||
|
||||
gram_prop = next(u for u in props.units if u.ifc_definition_id == gram_unit.id())
|
||||
assert gram_prop.name == "GRAM"
|
||||
assert gram_prop.unit_type == "MASSUNIT"
|
||||
assert gram_prop.is_assigned is False
|
||||
assert gram_prop.ifc_class == "IfcSIUnit"
|
||||
|
||||
minute_prop = next(u for u in props.units if u.ifc_definition_id == minute_unit.id())
|
||||
assert minute_prop.name == "minute"
|
||||
assert minute_prop.unit_type == "TIMEUNIT"
|
||||
assert minute_prop.is_assigned is True
|
||||
assert minute_prop.ifc_class == "IfcConversionBasedUnit"
|
||||
|
||||
hour_prop = next(u for u in props.units if u.ifc_definition_id == hour_unit.id())
|
||||
assert hour_prop.name == "hour"
|
||||
assert hour_prop.unit_type == "TIMEUNIT"
|
||||
assert hour_prop.is_assigned is False
|
||||
assert hour_prop.ifc_class == "IfcConversionBasedUnit"
|
||||
|
||||
day_prop = next(u for u in props.units if u.ifc_definition_id == day_unit.id())
|
||||
assert day_prop.name == "day"
|
||||
assert day_prop.unit_type == "TIMEUNIT"
|
||||
assert day_prop.is_assigned is False
|
||||
assert day_prop.ifc_class == "IfcConversionBasedUnit"
|
||||
|
||||
second_prop = next(u for u in props.units if u.ifc_definition_id == second_unit.id())
|
||||
assert second_prop.name == "SECOND"
|
||||
assert second_prop.unit_type == "TIMEUNIT"
|
||||
assert second_prop.is_assigned is False
|
||||
assert second_prop.ifc_class == "IfcSIUnit"
|
||||
|
||||
|
||||
class TestIsSceneUnitMetric(NewFile):
|
||||
def test_run(self):
|
||||
|
||||
@@ -32,7 +32,7 @@ def add_conversion_based_unit(
|
||||
function. You can choose from one of: inch, foot, yard, mile, square
|
||||
inch, square foot, square yard, acre, square mile, cubic inch, cubic
|
||||
foot, cubic yard, litre, fluid ounce UK, fluid ounce US, pint UK, pint
|
||||
US, gallon UK, gallon US, degree, ounce, pound, ton UK, ton US, lbf,
|
||||
US, gallon UK, gallon US, degree, ounce, pound, ton UK, ton US, tonne, lbf,
|
||||
kip, psi, ksi, minute, hour, day, btu, and fahrenheit.
|
||||
|
||||
:param name: A converted name chosen from the list above.
|
||||
@@ -61,7 +61,11 @@ def add_conversion_based_unit(
|
||||
dimensions = ifcopenshell.util.unit.named_dimensions[unit_type]
|
||||
exponents = file.createIfcDimensionalExponents(*dimensions)
|
||||
si_name = ifcopenshell.util.unit.si_type_names[unit_type]
|
||||
si_unit = file.createIfcSIUnit(UnitType=unit_type, Name=si_name)
|
||||
|
||||
if unit_type == "MASSUNIT":
|
||||
si_unit = file.createIfcSIUnit(UnitType=unit_type, Name=si_name, Prefix="KILO")
|
||||
else:
|
||||
si_unit = file.createIfcSIUnit(UnitType=unit_type, Name=si_name)
|
||||
|
||||
conversion_real = ifcopenshell.util.unit.si_conversions.get(name, 1)
|
||||
value_component = file.create_entity("IfcReal", **{"wrappedValue": conversion_real})
|
||||
|
||||
@@ -55,8 +55,12 @@ def assign_unit(
|
||||
length = ifcopenshell.api.unit.add_si_unit(model, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
area = ifcopenshell.api.unit.add_si_unit(model, unit_type="AREAUNIT")
|
||||
|
||||
# Make it our default units, if we are doing a metric building
|
||||
ifcopenshell.api.unit.assign_unit(model, units=[length, area])
|
||||
# Optionally, add mass and time units
|
||||
mass = ifcopenshell.api.unit.add_si_unit(model, unit_type="MASSUNIT", prefix="KILO")
|
||||
time = ifcopenshell.api.unit.add_si_unit(model, unit_type="TIMEUNIT")
|
||||
|
||||
# Make these the default units for the project
|
||||
ifcopenshell.api.unit.assign_unit(model, units=[length, area, mass, time])
|
||||
|
||||
# Alternatively, you may specify without any arguments to
|
||||
# automatically create millimeters, square meters, and cubic meters
|
||||
@@ -139,6 +143,7 @@ class Usecase:
|
||||
elif unit_type == "volume":
|
||||
dimensional_exponents = self.file.createIfcDimensionalExponents(3, 0, 0, 0, 0, 0, 0)
|
||||
name_prefix = "cubic"
|
||||
|
||||
si_unit = self.file.createIfcSIUnit(
|
||||
None,
|
||||
"{}UNIT".format(unit_type.upper()),
|
||||
|
||||
@@ -209,6 +209,7 @@ si_conversions = {
|
||||
"pound": 0.454,
|
||||
"ton UK": 1016.0469088,
|
||||
"ton US": 907.18474,
|
||||
"tonne": 1000.0,
|
||||
"lbf": 4.4482216153,
|
||||
"kip": 4448.2216153,
|
||||
"psi": 6894.7572932,
|
||||
@@ -253,6 +254,7 @@ imperial_types = {
|
||||
"pound": "MASSUNIT",
|
||||
"ton UK": "MASSUNIT",
|
||||
"ton US": "MASSUNIT",
|
||||
"tonne": "MASSUNIT",
|
||||
"lbf": "FORCEUNIT",
|
||||
"kip": "FORCEUNIT",
|
||||
"psi": "PRESSUREUNIT",
|
||||
@@ -323,6 +325,7 @@ unit_symbols = {
|
||||
"pound": "lb",
|
||||
"ton UK": "ton",
|
||||
"ton US": "ton",
|
||||
"tonne": "t",
|
||||
"lbf": "lbf",
|
||||
"kip": "kip",
|
||||
"psi": "psi",
|
||||
|
||||
@@ -40,6 +40,54 @@ class TestAddConversionBasedUnitIFC2X3(test.bootstrap.IFC2X3):
|
||||
assert si_unit.Prefix is None
|
||||
assert si_unit.Name == "METRE"
|
||||
|
||||
def test_adding_mass_units_creates_proper_massunit(self):
|
||||
mass_units = [
|
||||
("tonne", 1000.0),
|
||||
("pound", 0.454),
|
||||
("ounce", 0.02835),
|
||||
("ton UK", 1016.0469088),
|
||||
("ton US", 907.18474),
|
||||
]
|
||||
|
||||
for name, expected_conversion in mass_units:
|
||||
unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name)
|
||||
|
||||
assert unit.is_a("IfcConversionBasedUnit")
|
||||
assert unit.UnitType == "MASSUNIT"
|
||||
assert unit.Name == name
|
||||
|
||||
actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue
|
||||
assert actual_conversion == expected_conversion
|
||||
|
||||
target_unit = unit.ConversionFactor.UnitComponent
|
||||
assert target_unit.is_a("IfcSIUnit")
|
||||
assert target_unit.UnitType == "MASSUNIT"
|
||||
assert target_unit.Name == "GRAM"
|
||||
assert target_unit.Prefix == "KILO"
|
||||
|
||||
def test_adding_time_units_creates_proper_timeunit(self):
|
||||
time_units = [
|
||||
("minute", 60),
|
||||
("hour", 3600),
|
||||
("day", 86400),
|
||||
]
|
||||
|
||||
for name, expected_conversion in time_units:
|
||||
unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name=name)
|
||||
|
||||
assert unit.is_a("IfcConversionBasedUnit")
|
||||
assert unit.UnitType == "TIMEUNIT"
|
||||
assert unit.Name == name
|
||||
|
||||
actual_conversion = unit.ConversionFactor.ValueComponent.wrappedValue
|
||||
assert actual_conversion == expected_conversion
|
||||
|
||||
target_unit = unit.ConversionFactor.UnitComponent
|
||||
assert target_unit.is_a("IfcSIUnit")
|
||||
assert target_unit.UnitType == "TIMEUNIT"
|
||||
assert target_unit.Name == "SECOND"
|
||||
assert target_unit.Prefix is None
|
||||
|
||||
|
||||
class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBasedUnitIFC2X3):
|
||||
def test_adding_a_unit_with_offset(self):
|
||||
@@ -61,3 +109,8 @@ class TestAddConversionBasedUnitIFC4(test.bootstrap.IFC4, TestAddConversionBased
|
||||
assert si_unit.Prefix is None
|
||||
assert si_unit.Name == "KELVIN"
|
||||
assert unit.ConversionOffset == -459.67
|
||||
|
||||
def test_unknown_units_fall_back_to_userdefined(self):
|
||||
unknown_unit = ifcopenshell.api.unit.add_conversion_based_unit(self.file, name="unknown_unit")
|
||||
assert unknown_unit.UnitType == "USERDEFINED"
|
||||
assert unknown_unit.Name == "unknown_unit"
|
||||
|
||||
Reference in New Issue
Block a user