From a6271d09368201776b6d15a3bfc97d883891f00b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Jan 2023 14:33:56 +0500 Subject: [PATCH] IFC Window modifier Added IFC Window in object modifiers tab (works similar way to Ifc Stair) --- .../blenderbim/bim/module/model/__init__.py | 10 + .../blenderbim/bim/module/model/data.py | 21 + .../blenderbim/bim/module/model/prop.py | 84 ++++ .../bim/module/model/sverchok_modifier.py | 2 +- .../blenderbim/bim/module/model/ui.py | 119 ++++- .../blenderbim/bim/module/model/window.py | 377 ++++++++++++++++ .../api/geometry/add_window_representation.py | 413 ++++++++++++++++++ 7 files changed, 1023 insertions(+), 3 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/model/window.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 1e24ba138e..424bb1a477 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -27,6 +27,7 @@ from . import ( wall, slab, stair, + window, opening, pie, workspace, @@ -90,10 +91,12 @@ classes = ( prop.BIMArrayProperties, prop.BIMStairProperties, prop.BIMSverchokProperties, + prop.BIMWindowProperties, ui.BIM_PT_authoring, ui.BIM_PT_array, ui.BIM_PT_stair, ui.BIM_PT_sverchok, + ui.BIM_PT_window, ui.DisplayConstrTypesUI, ui.LaunchTypeManager, ui.HelpConstrTypes, @@ -116,6 +119,11 @@ classes = ( sverchok_modifier.DeleteSverchokGraph, sverchok_modifier.ImportSverchokGraph, sverchok_modifier.ExportSverchokGraph, + window.AddWindow, + window.CancelEditingWindow, + window.FinishEditingWindow, + window.EnableEditingWindow, + window.RemoveWindow, ) addon_keymaps = [] @@ -128,6 +136,7 @@ def register(): bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties) bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties) bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties) + bpy.types.Object.BIMWindowProperties = bpy.props.PointerProperty(type=prop.BIMWindowProperties) bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) bpy.types.VIEW3D_MT_add.append(ui.add_menu) @@ -147,6 +156,7 @@ def unregister(): del bpy.types.Object.BIMArrayProperties del bpy.types.Object.BIMStairProperties del bpy.types.Object.BIMSverchokProperties + del bpy.types.Object.BIMWindowProperties bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button) diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index 4ad36dbffb..3982a9d7ad 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -33,6 +33,7 @@ def refresh(): ArrayData.is_loaded = False StairData.is_loaded = False SverchokData.is_loaded = False + WindowData.is_loaded = False class AuthoringData: @@ -405,3 +406,23 @@ class SverchokData: return True except: return False + + +class WindowData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {"parameters": cls.parameters()} + + @classmethod + def parameters(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + if element: + psets = ifcopenshell.util.element.get_psets(element) + parameters = psets.get("BBIM_Window", None) + if parameters: + parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]") + return parameters \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 2636deccf7..51d6aef2c1 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -298,3 +298,87 @@ class BIMStairProperties(PropertyGroup): class BIMSverchokProperties(PropertyGroup): node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree) + + +def window_type_prop_update(self, context): + panels_data = self.window_types_panels[self.window_type] + for i in range(3): + try: + width, height = panels_data[i] + except IndexError: + width, height = (0.0, 0.0) + + self.relative_width[i] = width + self.relative_height[i] = height + +class BIMWindowProperties(PropertyGroup): + window_types = ( + ('SINGLE_PANEL', 'SINGLE_PANEL', ''), + ('DOUBLE_PANEL_HORIZONTAL', 'DOUBLE_PANEL_HORIZONTAL', ''), + ('DOUBLE_PANEL_VERTICAL', 'DOUBLE_PANEL_VERTICAL', ''), + ('TRIPLE_PANEL_BOTTOM', 'TRIPLE_PANEL_BOTTOM', ''), + ('TRIPLE_PANEL_TOP', 'TRIPLE_PANEL_TOP', ''), + ('TRIPLE_PANEL_LEFT', 'TRIPLE_PANEL_LEFT', ''), + ('TRIPLE_PANEL_RIGHT', 'TRIPLE_PANEL_RIGHT', ''), + ('TRIPLE_PANEL_HORIZONTAL', 'TRIPLE_PANEL_HORIZONTAL', ''), + ('TRIPLE_PANEL_VERTICAL', 'TRIPLE_PANEL_VERTICAL', ''), + ) + + # default panel relative dimensions + window_types_panels = { + 'SINGLE_PANEL': ((1.0, 1.0), ), + 'DOUBLE_PANEL_HORIZONTAL': ((1.0, 0.5), (1.0, 0.5), ), + 'DOUBLE_PANEL_VERTICAL': ((0.5, 1.0), (0.5, 1.0), ), + 'TRIPLE_PANEL_BOTTOM': ((0.5, 0.5), (0.5, 0.5), (1.0, 0.5), ), + 'TRIPLE_PANEL_TOP': ((1.0, 0.5), (0.5, 0.5), (0.5, 0.5), ), + 'TRIPLE_PANEL_LEFT': ((0.5, 1.0), (0.5, 0.5), (0.5, 0.5), ), + 'TRIPLE_PANEL_RIGHT': ((0.5, 0.5), (0.5, 1.0), (0.5, 0.5), ), + 'TRIPLE_PANEL_HORIZONTAL': ((1.0, 1/3), (1.0, 1/3), (1.0, 1/3),), + 'TRIPLE_PANEL_VERTICAL': ((1/3, 1.0), (1/3, 1.0), (1/3, 1.0),), + } + + is_editing: bpy.props.IntProperty(default=-1) + window_type: bpy.props.EnumProperty(name="Window Type", items=window_types, default="SINGLE_PANEL", update=window_type_prop_update) + overall_height: bpy.props.FloatProperty(name='Overall Height', default=900) + overall_width: bpy.props.FloatProperty(name='Overall Width', default=600) + + # lining properties + lining_depth: bpy.props.FloatProperty(name='Lining Depth', default=50) + lining_thickness: bpy.props.FloatProperty(name='Lining Thickness', default=50) + lining_offset: bpy.props.FloatProperty(name='Lining Offset', default=50) + lining_to_panel_offset_x: bpy.props.FloatProperty(name='Lining to Panel Offset X', default=25) + lining_to_panel_offset_y: bpy.props.FloatProperty(name='Lining to Panel Offset Y', default=25) + mullion_thickness: bpy.props.FloatProperty(name='(not used) Mullion Thickness') + transom_thickness: bpy.props.FloatProperty(name='(not used) Transom Thickness') + + # panel_properties + frame_depth: bpy.props.FloatVectorProperty(name='Frame Depth', size=3, default=[35]*3) + frame_thickness: bpy.props.FloatVectorProperty(name='Frame Thickness', size=3, default=[35]*3) + relative_width: bpy.props.FloatVectorProperty(name='Relative Width', size=3, default=[1,0,0]) + relative_height: bpy.props.FloatVectorProperty(name='Relative Height', size=3, default=[1,0,0]) + + def get_general_kwargs(self): + return { + "window_type": self.window_type, + "overall_height": self.overall_height, + "overall_width": self.overall_width, + } + + def get_lining_kwargs(self): + return { + "lining_depth": self.lining_depth, + "lining_thickness": self.lining_thickness, + "lining_offset": self.lining_offset, + "lining_to_panel_offset_x": self.lining_to_panel_offset_x, + "lining_to_panel_offset_y": self.lining_to_panel_offset_y, + "mullion_thickness": self.mullion_thickness, + "transom_thickness": self.transom_thickness, + } + + def get_panel_kwargs(self): + return { + "frame_depth": self.frame_depth, + "frame_thickness": self.frame_thickness, + "relative_width": self.relative_width, + "relative_height": self.relative_height, + } \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py index e0d154d6a1..8b693b2308 100644 --- a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py +++ b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py @@ -155,7 +155,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator): "edges": output_node.inputs["Edges"].sv_get(deepcopy=False, default=[]), "polygons": output_node.inputs["Polygons"].sv_get(deepcopy=False, default=[]), } - # sv_get returns array of arrays of 1 element, for convenience we reduce it to just 1 level array + # sv_get returns array of 1 element arrays, for convenience we reduce it to just 1 level array def try_first_element(x): return x[0] if x else x diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 80cf755b59..ae7f923c7e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -19,10 +19,10 @@ import bpy import blenderbim.tool as tool from bpy.types import Panel, Operator, Menu -from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData +from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData from blenderbim.bim.module.model.prop import store_cursor_position from blenderbim.bim.module.model.stair import update_stair_modifier -from blenderbim.bim.module.model.sverchok_modifier import update_sverchok_modifier +from blenderbim.bim.module.model.window import update_window_modifier_bmesh from blenderbim.bim.helper import prop_with_search @@ -406,6 +406,121 @@ class BIM_PT_sverchok(bpy.types.Panel): row.enabled = bool(props.node_group) +class BIM_PT_window(bpy.types.Panel): + bl_label = "IFC Window" + bl_idname = "BIM_PT_window" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "modifier" + + @classmethod + def poll(cls, context): + # always display modifier if it's IFC object + return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object) + + def draw(self, context): + if not WindowData.is_loaded: + WindowData.load() + + props = context.active_object.BIMWindowProperties + + if WindowData.data['parameters']: + row = self.layout.row(align=True) + row.label(text="Window parameters", icon="OUTLINER_OB_LATTICE") + + window_data = WindowData.data['parameters']['data'] + + if props.is_editing != - 1: + row = self.layout.row(align=True) + row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish editing") + row.operator("bim.cancel_editing_window", icon="CANCEL", text="") + + general_props = props.get_general_kwargs() + for prop in general_props: + self.layout.prop(props, prop) + + lining_props = props.get_lining_kwargs() + self.layout.label(text='Lining properties') + for prop in lining_props: + self.layout.prop(props, prop) + + panel_props = props.get_panel_kwargs() + self.layout.label(text='Panel properties') + number_of_panels = len(props.window_types_panels[props.window_type]) + + panel_box = self.layout.box() + row = panel_box.row() + cols = [row.column(align=True) for i in range(number_of_panels+1)] + + cols[0].label(text='') + + for panel_i in range(number_of_panels): + r = cols[panel_i+1].row() + r.alignment = 'CENTER' + r.label(text=f'#{panel_i}') + r = cols[panel_i+1].row() + + for prop in panel_props: + cols[0].label(text=f"{props.bl_rna.properties[prop].name}") + for panel_i in range(number_of_panels): + cols[panel_i+1].prop(props, prop, index=panel_i, text='') + + update_window_modifier_bmesh(context) + + else: + row.operator("bim.enable_editing_window", icon="GREASEPENCIL", text="") + row.operator("bim.remove_window", icon="X", text="") + + box = self.layout.box() + general_props = props.get_general_kwargs() + for prop in general_props: + prop_value = window_data[prop] + prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value + row = box.row(align=True) + row.label(text=f"{props.bl_rna.properties[prop].name}") + row.label(text=str(prop_value)) + + lining_props = props.get_lining_kwargs() + self.layout.label(text='Lining properties') + lining_box = self.layout.box() + for prop in lining_props: + prop_value = window_data['lining_properties'][prop] + prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value + row = lining_box.row(align=True) + row.label(text=f"{props.bl_rna.properties[prop].name}") + row.label(text=str(prop_value)) + + panel_props = props.get_panel_kwargs() + self.layout.label(text='Panel properties') + number_of_panels = len(props.window_types_panels[props.window_type]) + + panel_box = self.layout.box() + row = panel_box.row() + cols = [row.column(align=True) for i in range(number_of_panels+1)] + cols[0].label(text='') + + for panel_i in range(number_of_panels): + r = cols[panel_i+1].row() + r.alignment = 'CENTER' + r.label(text=f'#{panel_i}') + r = cols[panel_i+1].row() + + # TODO: align property values more evenly + for prop in panel_props: + cols[0].row().label(text=f"{props.bl_rna.properties[prop].name}") + for panel_i in range(number_of_panels): + r = cols[panel_i+1].row() + r.alignment = 'CENTER' + prop_value = window_data['panel_properties'][prop][panel_i] + prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value + r.label(text=str(prop_value)) + r = cols[panel_i+1].row() + + else: + row = self.layout.row() + row.label(text="No Window Found") + row.operator("bim.add_window", icon="ADD", text="") + class BIM_MT_model(Menu): bl_idname = "BIM_MT_model" bl_label = "IFC Objects" diff --git a/src/blenderbim/blenderbim/bim/module/model/window.py b/src/blenderbim/blenderbim/bim/module/model/window.py new file mode 100644 index 0000000000..a71920869a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/window.py @@ -0,0 +1,377 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021, 2022 Dion Moult , @Andrej730 +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + + +import bpy +from bpy.types import Operator +from bpy.props import FloatProperty, IntProperty, BoolProperty +from bpy_extras.object_utils import AddObjectHelper, object_data_add +import bmesh +from bmesh.types import BMVert + +import ifcopenshell +from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS +import blenderbim +import blenderbim.tool as tool +import blenderbim.core.geometry as core +from blenderbim.bim.ifc import IfcStore + +from mathutils import Vector +from pprint import pprint + +from os.path import basename, dirname +import json + + +V = lambda *x: Vector([float(i) for i in x]) + +def update_window_modifier_representation(context): + obj = context.active_object + props = obj.BIMWindowProperties + ifc_file = tool.Ifc.get() + representation_data = { + 'partition_type': props.window_type, + 'overall_height': props.overall_height, + 'overall_width': props.overall_width, + 'lining_properties': { + 'LiningDepth': props.lining_depth, + 'LiningThickness': props.lining_thickness, + 'LiningOffset': props.lining_offset, + 'LiningToPanelOffsetX': props.lining_to_panel_offset_x, + 'LiningToPanelOffsetY': props.lining_to_panel_offset_y, + }, + 'panel_properties': [] + } + number_of_panels = len(props.window_types_panels[props.window_type]) + for panel_i in range(number_of_panels): + panel_data = { + 'FrameDepth': props.frame_depth[panel_i], + 'FrameThickness': props.frame_thickness[panel_i], + 'RelativeWidth': props.relative_width[panel_i], + 'RelativeHeight': props.relative_height[panel_i], + } + representation_data['panel_properties'].append(panel_data) + + # ELEVATION_VIEW representation + ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW") + if not ifc_context: + model_context = ifcopenshell.util.representation.get_context(ifc_file, 'Model') + ifc_context = ifcopenshell.api.run( + 'context.add_context', + ifc_file, + context_type='Model', + context_identifier='Profile', + target_view='ELEVATION_VIEW', + parent=model_context) + + representation_data['context'] = ifc_context + elevation_representation = ifcopenshell.api.run("geometry.add_window_representation", ifc_file, **representation_data) + replace_representation_for_object(ifc_file, ifc_context, obj, elevation_representation) + + # MODEL_VIEW representation + ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + representation_data['context'] = ifc_context + model_representation = ifcopenshell.api.run("geometry.add_window_representation", ifc_file, **representation_data) + replace_representation_for_object(ifc_file, ifc_context, obj, model_representation) + +def replace_representation_for_object(ifc_file, ifc_context, obj, new_representation): + ifc_element = tool.Ifc.get_entity(obj) + old_representation = ifcopenshell.util.representation.get_representation( + ifc_element, + ifc_context.ContextType, + ifc_context.ContextIdentifier, + ifc_context.TargetView) + + if old_representation: + for inverse in ifc_file.get_inverse(old_representation): + ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation) + core.switch_representation( + tool.Geometry, + obj=obj, + representation=new_representation, + should_reload=True, + is_global=False, + should_sync_changes_first=True + ) + core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation) + else: + ifcopenshell.api.run("geometry.assign_representation", ifc_file, product=ifc_element, representation=new_representation) + +def create_bm_window_closed_profile(bm, size: Vector, thickness:float, position:Vector): + width, depth, height = size + + verts = [ + (0, [thickness, 0.0, thickness]), + (1, [width-thickness, 0.0, thickness]), + (2, [thickness, 0.0, height-thickness]), + (3, [width-thickness, 0.0, height-thickness]), + (4, [0.0, 0.0, 0.0]), + (5, [0.0, 0.0, height]), + (6, [width, 0.0, 0.0]), + (7, [width, 0.0, height]), + ] + + edges = [ + (0, (0, 1)), + (1, (2, 3)), + (2, (4, 5)), + (3, (6, 7)), + (4, (7, 5)), + (5, (1, 3)), + (6, (4, 6)), + (7, (0, 2)), + (8, (2, 5)), + (9, (3, 7)), + (10, (4, 0)), + (11, (1, 6)), + ] + + faces = [ + (0, (2, 3, 7, 5)), + (1, (5, 4, 0, 2)), + (2, (1, 0, 4, 6)), + (3, (7, 3, 1, 6)), + ] + + bm.verts.index_update() + bm.edges.index_update() + bm.faces.ensure_lookup_table() + + new_verts = [bm.verts.new(v[1]) for v in verts] + new_edges = [bm.edges.new([new_verts[vi] for vi in edge[1]]) for edge in edges] + new_faces = [bm.faces.new([new_verts[vi] for vi in face[1]]) for face in faces] + + extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces) + extrusion_vector = Vector((0, 1, 0)) * depth + translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)] + bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts) + + bmesh.ops.translate(bm, vec=position, verts=new_verts+translate_verts) + + return new_verts + + +def update_window_modifier_bmesh(context): + obj = context.object + props = obj.BIMWindowProperties + si_conversion = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + panel_schema = DEFAULT_PANEL_SCHEMAS[props.window_type] + accumulated_height = [0] * len(panel_schema[0]) + built_panels = [] + + overall_width = props.overall_width * si_conversion + lining_depth = props.lining_depth * si_conversion + overall_height = props.overall_height * si_conversion + lining_to_panel_offset_x = props.lining_to_panel_offset_x * si_conversion + + bm = bmesh.new() + + for row_i, panel_row in enumerate(reversed(panel_schema)): + accumulated_width = 0 + for column_i, panel_i in enumerate(panel_row): + if panel_i in built_panels: + accumulated_height[column_i] += props.relative_height[panel_i] + accumulated_width += props.relative_width[panel_i] + continue + + frame_depth = props.frame_depth[panel_i] * si_conversion + frame_thickness = props.frame_thickness[panel_i] * si_conversion + glass_thickness = 10 * si_conversion + + # create lining + lining_size = V( + overall_width * props.relative_width[panel_i], + lining_depth, + overall_height* props.relative_height[panel_i]) + thickness = props.lining_thickness * si_conversion + lining_verts = create_bm_window_closed_profile(bm, lining_size, thickness, V(0, 0, 0)) + + # add panel + panel_size = lining_size.copy() + panel_size.y = frame_depth + panel_size = panel_size - V(lining_to_panel_offset_x*2, 0, lining_to_panel_offset_x*2) + + panel_position = V( + props.lining_to_panel_offset_x * si_conversion, + ((props.lining_depth-props.frame_depth[panel_i]) + props.lining_to_panel_offset_y) * si_conversion, + props.lining_to_panel_offset_x * si_conversion, + ) + thickness = props.frame_thickness[panel_i] * si_conversion + panel_verts = create_bm_window_closed_profile(bm, panel_size, thickness, panel_position) + + # add glass + glass_verts = bmesh.ops.create_cube(bm, size=1)['verts'] + bmesh.ops.translate(bm, vec=-glass_verts[0].co, verts=glass_verts) + glass_size = panel_size + glass_size.y = glass_thickness + glass_size = glass_size - V(frame_thickness*2, 0, frame_thickness) + glass_position = panel_position + V( + frame_thickness, + frame_depth/2-glass_thickness/2, + frame_thickness, + ) + bmesh.ops.scale(bm, vec=glass_size, verts=glass_verts) + bmesh.ops.translate(bm, vec=glass_position, verts=glass_verts) + + # translate panel + accumulated_offset = V(accumulated_width, 0, accumulated_height[column_i]) * V(overall_width, 0, overall_height) + bmesh.ops.translate(bm, + vec=accumulated_offset, + verts=lining_verts + panel_verts + glass_verts) + + built_panels.append(panel_i) + + accumulated_height[column_i] += props.relative_height[panel_i] + accumulated_width += props.relative_width[panel_i] + + bmesh.ops.translate(bm, + vec=V(0, props.lining_offset * si_conversion, 0), + verts=bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + + if bpy.context.object.mode == "EDIT": + bmesh.update_edit_mesh(obj.data) + else: + bm.to_mesh(obj.data) + bm.free() + obj.data.update() + + +# UI operators +class AddWindow(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_window" + bl_label = "Add Window" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMWindowProperties + + window_data = props.get_general_kwargs() + lining_props = props.get_lining_kwargs() + panel_props = props.get_panel_kwargs() + + window_data['lining_properties'] = lining_props + window_data['panel_properties'] = panel_props + psets = ifcopenshell.util.element.get_psets(element) + pset = psets.get("BBIM_Window", None) + + if pset: + pset = tool.Ifc.get().by_id(pset["id"]) + else: + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Window") + + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Data": json.dumps(window_data, default=list)}, + ) + update_window_modifier_representation(context) + return {"FINISHED"} + + +class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.cancel_editing_window" + bl_label = "Cancel editing Window" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + psets = ifcopenshell.util.element.get_psets(element) + data = json.loads(psets["BBIM_Window"]["Data"]) + props = obj.BIMWindowProperties + # restore previous settings since editing was canceled + for prop_name in data: + setattr(props, prop_name, data[prop_name]) + update_window_modifier_representation(context) + + props.is_editing = -1 + + return {"FINISHED"} + + +class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.finish_editing_window" + bl_label = "Finish editing window" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMWindowProperties + + psets = ifcopenshell.util.element.get_psets(element) + pset = psets["BBIM_Window"] + window_data = props.get_general_kwargs() + lining_props = props.get_lining_kwargs() + panel_props = props.get_panel_kwargs() + + window_data['lining_properties'] = lining_props + window_data['panel_properties'] = panel_props + + props.is_editing = -1 + + update_window_modifier_representation(context) + + pset = tool.Ifc.get().by_id(pset["id"]) + window_data = json.dumps(window_data, default=list) + # TODO: debug two types of data in + # from blenderbim.bim.module.model.data import WindowData + # WindowData.data['parameters'].keys() + # same thing could go for both stairs and arrays + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": window_data}) + return {"FINISHED"} + + +class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_window" + bl_label = "Enable Editing Window" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + props = obj.BIMWindowProperties + element = tool.Ifc.get_entity(obj) + pset = ifcopenshell.util.element.get_psets(element) + data = json.loads(pset["BBIM_Window"]["Data"]) + # required since we could load pset from .ifc and BIMWindowProperties won't be set + for prop_name in data: + setattr(props, prop_name, data[prop_name]) + + props.is_editing = 1 + return {'FINISHED'} + + +class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_window" + bl_label = "Remove Window" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + obj.BIMWindowProperties.is_editing = -1 + + pset = ifcopenshell.util.element.get_psets(element) + pset = tool.Ifc.get().by_id(pset["BBIM_Window"]["id"]) + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) + + return {"FINISHED"} diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py new file mode 100644 index 0000000000..d159f64453 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -0,0 +1,413 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 @Andrej730 +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell.util.unit +from math import sin, cos +from ifcopenshell.util.shape_builder import ShapeBuilder, V +from mathutils import Vector + + +# SCHEMAS describe panels setup +# where: +# - schema rows represent window X axis +# - schema columns represent window Y axis +# - order of rows is from top of the window to bottom + +DEFAULT_PANEL_SCHEMAS = { + 'SINGLE_PANEL': [[0]], + 'DOUBLE_PANEL_HORIZONTAL': [[0],[1]], + 'DOUBLE_PANEL_VERTICAL': [[0,1]], + 'TRIPLE_PANEL_BOTTOM': [[0,1],[2,2]], + 'TRIPLE_PANEL_TOP': [[0,0], [1,2]], + 'TRIPLE_PANEL_LEFT': [[0,1],[0,2]], + 'TRIPLE_PANEL_RIGHT': [[0,1],[2,1]], + 'TRIPLE_PANEL_HORIZONTAL': [[0],[1],[2]], + 'TRIPLE_PANEL_VERTICAL': [[0,1,2]], +} + +class Usecase: + def __init__(self, file, **settings): + self.file = file + # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindow.htm + # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowTypePartitioningEnum.htm + # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowLiningProperties.htm + # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowPanelProperties.htm + self.settings = { + "context": None, # IfcGeometricRepresentationContext + # SINGLE_PANEL, DOUBLE_PANEL_HORIZONTAL, DOUBLE_PANEL_VERTICAL, + # TRIPLE_PANEL_BOTTOM, TRIPLE_PANEL_HORIZONTAL, TRIPLE_PANEL_LEFT, TRIPLE_PANEL_RIGHT, TRIPLE_PANEL_TOP, TRIPLE_PANEL_VERTICAL + "partition_type": 'SINGLE_PANEL', + "overall_height": 900, + "overall_width": 600, + "lining_properties": { + 'LiningDepth': 50, + 'LiningThickness': 50, + 'LiningOffset': 50, # offset to the wall + 'LiningToPanelOffsetX': 25, + 'LiningToPanelOffsetY': 25, + # applies to DoublePanelVertical, TriplePanelBottom, TriplePanelTop, TriplePanelLeft, TriplePanelRight + # mullion - distance between panels + 'FirstMullionOffset': ..., # distance from the first lining to the mullion + # TODO: take mullion thickness into account + 'MullionThickness': ..., + # applies to DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop, TriplePanelLeft, TriplePanelRight + # works similar way to mullion + 'FirstTransomOffset': ..., + 'TransomThickness': ..., + # applies to TriplePanelVertical + 'SecondMullionOffset': ..., # distance from the first lining to the second mullion + # applies to TriplePanelHorizontal + 'SecondTransomOffset': ..., + 'ShapeAspectStyle': None, # DEPRECATED + }, + "panel_properties": [ + { + 'FrameDepth': 35, # by Y + 'FrameThickness': 35, # by X + # BOTTOM, LEFT, MIDDLE, RIGHT, TOP + 'PanelPosition': ..., + # defines the basic ways to describe how window panels operate + # how it's hanged, how it opens + 'OperationType': None, + 'ShapeAspectStyle': None, # DEPRECATED + + # Custom Parameter not available in IFC + # dimensions of the panel relative to overall window dimensions + 'RelativeWidth': 1.0, + 'RelativeHeight': 1.0, + }, + ] + } + + for key, value in settings.items(): + self.settings[key] = value + self.settings["panel_schema"] = DEFAULT_PANEL_SCHEMAS[self.settings['partition_type']] + + # recalculate relative width and height to avoid errors + # TODO: rework or remove + # panels_data = self.settings['panel_properties'] + # current_height = sum(p['RelativeHeight'] for p in panels_data) + # current_width = sum(p['RelativeWidth'] for p in panels_data) + + # for p in panels_data: + # if current_height != 1.0: + # p['RelativeHeight'] = p['RelativeHeight'] / current_height + + # if current_width != 1.0: + # p['RelativeWidth'] = p['RelativeWidth'] / current_width + + + + def execute(self): + self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) + builder = ShapeBuilder(self.file) + overall_height = self.convert_si_to_unit(self.settings['overall_height']) + overall_width = self.convert_si_to_unit(self.settings['overall_width']) + + if self.settings['context'].TargetView == 'ELEVATION_VIEW': + rect = builder.rectangle(V(overall_width, 0, overall_height)) + representation_evelevation = builder.get_representation(self.settings['context'], rect) + return representation_evelevation + + + panel_schema = self.settings['panel_schema'] + panels = self.settings['panel_properties'] + accumulated_height = [0] * len(panel_schema[0]) + built_panels = [] + window_items = [] + + lining_thickness = self.convert_si_to_unit(self.settings['lining_properties']['LiningThickness']) + lining_depth = self.convert_si_to_unit(self.settings['lining_properties']['LiningDepth']) + lining_offset = self.convert_si_to_unit(self.settings['lining_properties']['LiningOffset']) + lining_panel_offset_x = self.convert_si_to_unit(self.settings['lining_properties']['LiningToPanelOffsetX']) + lining_panel_offset_y = self.convert_si_to_unit(self.settings['lining_properties']['LiningToPanelOffsetY']) + glass_thickness = self.convert_si_to_unit(10) + + for row_i, panel_row in enumerate(reversed(panel_schema)): + accumulated_width = 0 + for column_i, panel_i in enumerate(panel_row): + if panel_i in built_panels: + accumulated_height[column_i] += cur_panel['RelativeHeight'] + accumulated_width += cur_panel['RelativeWidth'] + continue + cur_panel = panels[panel_i] + current_items = [] + panel_depth = self.convert_si_to_unit(cur_panel['FrameDepth']) + panel_thickness = self.convert_si_to_unit(cur_panel['FrameThickness']) + panel_height = cur_panel['RelativeHeight'] * overall_height + panel_width = cur_panel['RelativeWidth'] * overall_width + + panel_actual_width = panel_width-lining_panel_offset_x*2 + panel_actual_height = panel_height-lining_panel_offset_x*2 + + glass_width = panel_actual_width - panel_thickness*2 + glass_height = panel_actual_height - panel_thickness*2 + + # build lining + lining_items_vertical = [] + lining_items = [] + # lining is calculated on panel level because + # panel depth is used + lining_rectangle = builder.rectangle( size=V(lining_thickness, lining_depth) ) + + # need to check offsets to decide whether lining should be rectangle + # or L shaped + if lining_panel_offset_x >= lining_thickness \ + or lining_panel_offset_y >= lining_depth: + lining_vertical_polyline = ifcopenshell.util.element.copy_deep(self.file, lining_rectangle) + lining_vertical_height = panel_height + + else: + lining_points = [ + V(0, 0), + V(0, lining_depth), + V(lining_panel_offset_x, lining_depth), + V(lining_panel_offset_x, lining_depth-(panel_depth-lining_panel_offset_y)), + V(lining_thickness, lining_depth-(panel_depth-lining_panel_offset_y)), + V(lining_thickness, 0), + ] + + # lining vertical + lining_vertical_polyline = builder.polyline(lining_points, closed=True) + lining_vertical_height = panel_height - lining_panel_offset_x * 2 + + # if lining panel X offset is present + # then we also need to add two more box shapes + # to finish the lining after the panel ends + if lining_panel_offset_x > 0: + lining_vertical_addition = builder.extrude(builder.deep_copy(lining_rectangle), lining_panel_offset_x) + + lining_items_vertical.extend([ + lining_vertical_addition, + builder.translate(lining_vertical_addition, V(0,0,panel_height - lining_panel_offset_x), create_copy=True) + ]) + + # horizontal lining + lining_horizontal_polyline = builder.deep_copy(lining_vertical_polyline) + lining_horizontal_extruded = builder.extrude( + lining_horizontal_polyline, + magnitude=panel_width-2*lining_thickness, + extrusion_vector=V(0,0,-1), + position_z_axis=V(-1,0,0), + position_x_axis=V(0,0,1), + position=V(lining_thickness, 0, 0) + ) + + # TODO: should implement mirror by Z for more readability + # TODO: investigate meaning of mirror axes in case of custom x/y/z space + # lining_horizontal_mirrored = builder.mirror( + # lining_horizontal_extruded, + # mirror_point=V(0, panel_height/2), + # mirror_axes=V(0,1), + # create_copy=True + # ) + + lining_horizontal_polyline_mirrored = builder.mirror( + lining_horizontal_polyline, + mirror_axes=V(1,0), + mirror_point=V(lining_thickness,0), + create_copy=True + ) + lining_horizontal_mirrored = builder.extrude( + lining_horizontal_polyline_mirrored, + magnitude=panel_width-2*lining_thickness, + extrusion_vector=V(0,0,-1), + position_z_axis=V(-1,0,0), + position_x_axis=V(0,0,1), + position=V(lining_thickness, 0, panel_height - lining_thickness*2) + ) + lining_items.extend([lining_horizontal_extruded, lining_horizontal_mirrored]) + + extrusion_position = V(0,0,lining_panel_offset_x) + lining_vertical_extruded = builder.extrude(lining_vertical_polyline, lining_vertical_height, position=extrusion_position) + lining_items_vertical.append(lining_vertical_extruded) + + lining_items_vertical_mirrored = builder.mirror( + lining_items_vertical, mirror_point=V(panel_width/2, 0), + mirror_axes=V(1,0), + create_copy=True) + + lining_items.extend(lining_items_vertical) + lining_items.extend(lining_items_vertical_mirrored) + current_items.extend(lining_items) + + # PANEL + panel_items = [] + + panel_position = V( + lining_panel_offset_x, + (lining_depth-panel_depth) + lining_panel_offset_y, + lining_panel_offset_x) + panel_rect = builder.rectangle(size=V(panel_actual_width, 0, panel_actual_height)) + glass_rect = builder.rectangle( + size=V(glass_width, 0, glass_height), + position=V(panel_thickness, 0, panel_thickness)) + panel_profile = builder.profile(panel_rect, inner_curves=glass_rect) + panel_extruded = builder.extrude( + panel_profile, + panel_depth, + extrusion_vector=V(0,1,0), + position=panel_position) + panel_items.append(panel_extruded) + + current_items.extend(panel_items) + + # add glass + glass_position = panel_position + V(0, panel_depth/2-glass_thickness/2, 0) + glass_rect = builder.deep_copy(glass_rect) + glass = builder.extrude( + glass_rect, + glass_thickness, + extrusion_vector=V(0,1,0), + position=glass_position + ) + current_items.append(glass) + + # translate panel + accumulated_offset = V(accumulated_width, 0, accumulated_height[column_i]) * V(overall_width, 0, overall_height) + builder.translate(current_items, accumulated_offset) + + built_panels.append(panel_i) + window_items.extend(current_items) + + accumulated_height[column_i] += cur_panel['RelativeHeight'] + accumulated_width += cur_panel['RelativeWidth'] + + builder.translate(window_items, V(0, lining_offset, 0)) # wall offset + representation = builder.get_representation(self.settings['context'], window_items) + return representation + + def convert_si_to_unit(self, value): + return value * 0.001 / self.settings["unit_scale"] + +# TODO: remove test at the end +if __name__ == "__main__": + ifc_file = ifcopenshell.file() + project = ifcopenshell.api.run( + "root.create_entity", ifc_file, ifc_class="IfcProject", name=f"Non-structural assets library" + ) + library = ifcopenshell.api.run( + "root.create_entity", ifc_file, ifc_class="IfcProjectLibrary", name=f"Non-structural assets library" + ) + ifcopenshell.api.run("project.assign_declaration", ifc_file, definition=library, relating_context=project) + unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI") + ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit]) + model = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Model") + plan = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Plan") + + representations = { + "body": ifcopenshell.api.run( + "context.add_context", + ifc_file, + context_type="Model", + context_identifier="Body", + target_view="MODEL_VIEW", + parent=model, + ), + "elevation": ifcopenshell.api.run( + "context.add_context", + ifc_file, + context_type="Model", + context_identifier="Profile", + target_view="ELEVATION_VIEW", + parent=model, + ), + "annotation": ifcopenshell.api.run( + "context.add_context", + ifc_file, + context_type="Plan", + context_identifier="Annotation", + target_view="PLAN_VIEW", + parent=plan, + ), + } + + + settings = { + 'context': representations['body'], + # SINGLE_PANEL, DOUBLE_PANEL_HORIZONTAL, DOUBLE_PANEL_VERTICAL, + # TRIPLE_PANEL_BOTTOM, TRIPLE_PANEL_HORIZONTAL, TRIPLE_PANEL_LEFT, TRIPLE_PANEL_RIGHT, TRIPLE_PANEL_TOP, TRIPLE_PANEL_VERTICAL + "partition_type": 'TRIPLE_PANEL_RIGHT', + "overall_height": 900, + "overall_width": 600*3, + # "lining_properties": { + # 'LiningDepth': 50, + # 'LiningThickness': 50, + # 'LiningOffset': 50, # offset to the wall + # 'LiningToPanelOffsetX': 25, + # 'LiningToPanelOffsetY': 25, + # # applies to DoublePanelVertical, TriplePanelBottom, TriplePanelTop, TriplePanelLeft, TriplePanelRight + # # mullion - distance between panels + # 'FirstMullionOffset': ..., # distance from the first lining to the mullion + # # TODO: take mullion thickness into account + # 'MullionThickness': ..., + # # applies to DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop, TriplePanelLeft, TriplePanelRight + # # works similar way to mullion + # 'FirstTransomOffset': ..., + # 'TransomThickness': ..., + # # applies to TriplePanelVertical + # 'SecondMullionOffset': ..., # distance from the first lining to the second mullion + # # applies to TriplePanelHorizontal + # 'SecondTransomOffset': ..., + # 'ShapeAspectStyle': None, # DEPRECATED + # }, + "panel_properties": [ + { + 'FrameDepth': 35, # by Y + 'FrameThickness': 35, # by X + 'RelativeWidth': 1.0/2, + 'RelativeHeight': 1.0/2, + }, + { + 'FrameDepth': 35, # by Y + 'FrameThickness': 35, # by X + 'RelativeWidth': 1.0/2, + 'RelativeHeight': 1.0, + }, + { + 'FrameDepth': 35, # by Y + 'FrameThickness': 35, # by X + 'RelativeWidth': 1.0/2, + 'RelativeHeight': 1.0/2, + }, + ] + } + # builder = ShapeBuilder(ifc_file) + # points = [V(0,0), V(5,1)] + # print(points) + # base_point = V(2,2) + # points = [p + base_point for p in points] + # points = [builder.mirror_2d_point(p, mirror_axes=V(0,1), mirror_point=V(3,3)) for p in points] + # print('mirrored') + # print(points) + # print('mirrored base point') + # base_point = builder.mirror_2d_point(base_point, mirror_axes=V(0,1), mirror_point=V(3,3)) + # print(base_point) + # print('base line') + # print([p - base_point for p in points]) + + use_case = Usecase(ifc_file, **settings) + representation = use_case.execute() + print(representation) + + settings['context'] = representations['elevation'] + use_case = Usecase(ifc_file, **settings) + representation_2d = use_case.execute() + print(representation_2d) + + ifc_file.write("tmp.ifc") \ No newline at end of file