IFC Window modifier

Added IFC Window in object modifiers tab (works similar way to Ifc Stair)
This commit is contained in:
Andrej730
2023-01-11 14:33:56 +05:00
parent cf6acd5dd6
commit a6271d0936
7 changed files with 1023 additions and 3 deletions
@@ -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)
@@ -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
@@ -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,
}
@@ -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
@@ -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"
@@ -0,0 +1,377 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @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 <http://www.gnu.org/licenses/>.
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"}
@@ -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 <http://www.gnu.org/licenses/>.
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")