Door modifier - basic setup

Also:
- refactored window modifier code - part of it is now reused building a door and can be used by other module in the future
- fixed bug with window lining intersecting frame when lining_depth < panel_depth
- simplified the way lining is built in ifc - previously it was using 4-6 extrusions, now it's 1-2 which is also better for 2D drawings
This commit is contained in:
Andrej730
2023-01-31 15:10:37 +05:00
parent a8400ec963
commit b2bdbdd8c3
8 changed files with 1251 additions and 220 deletions
@@ -33,6 +33,7 @@ from . import (
workspace,
profile,
sverchok_modifier,
door
)
classes = (
@@ -92,11 +93,13 @@ classes = (
prop.BIMStairProperties,
prop.BIMSverchokProperties,
prop.BIMWindowProperties,
prop.BIMDoorProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
ui.BIM_PT_window,
ui.BIM_PT_door,
ui.DisplayConstrTypesUI,
ui.LaunchTypeManager,
ui.HelpConstrTypes,
@@ -125,6 +128,12 @@ classes = (
window.FinishEditingWindow,
window.EnableEditingWindow,
window.RemoveWindow,
door.BIM_OT_add_door,
door.AddDoor,
door.CancelEditingDoor,
door.FinishEditingDoor,
door.EnableEditingDoor,
door.RemoveDoor,
)
addon_keymaps = []
@@ -138,9 +147,11 @@ def register():
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.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
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_mesh_add.append(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
wm = bpy.context.window_manager
@@ -159,10 +170,12 @@ def unregister():
del bpy.types.Object.BIMStairProperties
del bpy.types.Object.BIMSverchokProperties
del bpy.types.Object.BIMWindowProperties
del bpy.types.Object.BIMDoorProperties
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)
bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
@@ -34,6 +34,7 @@ def refresh():
StairData.is_loaded = False
SverchokData.is_loaded = False
WindowData.is_loaded = False
DoorData.is_loaded = False
class AuthoringData:
@@ -426,3 +427,23 @@ class WindowData:
if parameters:
parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
class DoorData:
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_Door", None)
if parameters:
parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]")
return parameters
@@ -0,0 +1,504 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 @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
import blenderbim
import blenderbim.tool as tool
import blenderbim.core.geometry as core
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.window import create_bm_window_frame, create_bm_window, create_bm_box
from mathutils import Vector
from pprint import pprint
from os.path import basename, dirname
import json
import collections
V = lambda *x: Vector([float(i) for i in x])
def update_door_modifier_representation(context):
obj = context.active_object
props = obj.BIMDoorProperties
ifc_file = tool.Ifc.get()
representation_data = {
"partition_type": props.door_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,
"TransomThickness": props.transom_thickness,
"TransomOffset": props.transom_offset,
"transomThickness": props.transom_thickness,
"TransomOffset": props.transom_offset,
"CasingThickness": props.casing_thickness,
"CasingDepth": props.casing_depth,
"ThresholdThickness": props.threshold_thickness,
"ThresholdDepth": props.threshold_depth,
"ThresholdOffset": props.threshold_offset,
},
"panel_properties": {
"PanelDepth": props.panel_depth,
"PanelWidth": props.panel_width_ratio,
"FrameDepth": props.frame_depth,
"FrameThickness": props.frame_thickness,
},
}
# 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_door_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_door_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.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
)
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_representation,
should_reload=True,
is_global=False,
should_sync_changes_first=True,
)
def create_bm_door_lining(bm, size: Vector, thickness: Vector, position:Vector=V(0,0,0).freeze()):
"""`thickness` of the profile is defined as list in the following order: `(SIDE, TOP)`
`thickness` can be also defined just as 1 float value.
"""
if not isinstance(thickness, collections.abc.Iterable):
thickness = [thickness] * 2
th_side, th_up = thickness
width, depth, height = size
verts = [
(0, [width - th_side, 0.0, height-th_up]),
(1, [0.0, 0.0, height]),
(2, [th_side, 0.0, height-th_up]),
(3, [0.0, 0.0, 0.0]),
(4, [width - th_side, 0.0, 0.0]),
(5, [width, 0.0, height]),
(6, [th_side, 0.0, 0.0]),
(7, [width, 0.0, 0.0])
]
edges = [
(0, [5, 7]),
(1, [0, 2]),
(2, [1, 5]),
(3, [4, 0]),
(4, [2, 1]),
(5, [0, 5]),
(6, [4, 7]),
(7, [3, 1]),
(8, [3, 6]),
(9, [2, 6]),
]
faces = [
(0, [5, 0, 2, 1]),
(1, [4, 0, 5, 7]),
(2, [3, 1, 2, 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 + translate_verts
def update_door_modifier_bmesh(context):
obj = context.object
props = obj.BIMDoorProperties
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
overall_width = props.overall_width * si_conversion
overall_height = props.overall_height * si_conversion
# lining params
lining_depth = props.lining_depth * si_conversion
lining_thickness_default = props.lining_thickness * si_conversion
lining_offset = props.lining_offset * si_conversion
lining_to_panel_offset_x = props.lining_to_panel_offset_x * si_conversion
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion
transom_thickness = props.transom_thickness * si_conversion / 2
transfom_offset = props.transom_offset * si_conversion
if transom_thickness == 0:
transfom_offset = 0
window_lining_height = overall_height - transfom_offset - transom_thickness
top_lining_thickness = transom_thickness or lining_thickness_default
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
door_opening_width = (overall_width - lining_to_panel_offset_x * 2)
threshold_thickness = props.threshold_thickness * si_conversion
threshold_depth = props.threshold_depth * si_conversion
threshold_offset = props.threshold_offset * si_conversion
threshold_width = overall_width - lining_thickness_default * 2
casing_thickness = props.casing_thickness * si_conversion
casing_depth = props.casing_depth * si_conversion
# panel params
panel_depth = props.panel_depth * si_conversion
panel_width = door_opening_width * props.panel_width_ratio
frame_depth = props.frame_depth * si_conversion
frame_thickness = props.frame_thickness * si_conversion
frame_height = window_lining_height - lining_to_panel_offset_x * 2
glass_thickness = 0.01 * si_conversion
if transfom_offset:
panel_height = transfom_offset + transom_thickness - lining_to_panel_offset_x - threshold_thickness
lining_height = transfom_offset + transom_thickness
else:
panel_height = overall_height - lining_to_panel_offset_x - threshold_thickness
lining_height = overall_height
bm = bmesh.new()
# add lining
lining_size = V(overall_width, lining_depth, lining_height)
lining_thickness = [lining_thickness_default, top_lining_thickness]
lining_verts = create_bm_door_lining(bm, lining_size, lining_thickness)
# add threshold
if not threshold_thickness:
threshold_verts = []
else:
threshold_size = V(threshold_width, threshold_depth, threshold_thickness)
threshold_position = V(lining_thickness_default, threshold_offset, 0)
threshold_verts = create_bm_box(bm, threshold_size, threshold_position)
# add casings
casing_verts = []
if not lining_offset and casing_thickness:
casing_wall_overlap = max(casing_thickness - lining_thickness_default, 0)
casing_size = V(overall_width + casing_wall_overlap*2, casing_depth, overall_height+casing_wall_overlap)
casing_position = V(-casing_wall_overlap, -casing_depth, 0)
outer_casing_verts = create_bm_door_lining(bm, casing_size, casing_thickness, casing_position)
inner_casing_thickness = [
casing_thickness-panel_lining_overlap_x,
casing_thickness-panel_top_lining_overlap_x
]
inner_casing_position = V(-casing_wall_overlap, lining_depth, 0)
inner_casing_verts = create_bm_door_lining(bm, casing_size, inner_casing_thickness, inner_casing_position)
casing_verts.extend([outer_casing_verts, inner_casing_verts])
# add door panel
panel_size = V(
panel_width,
panel_depth,
panel_height
)
panel_position = V(
lining_to_panel_offset_x,
lining_to_panel_offset_y,
threshold_thickness
)
panel_verts = create_bm_box(bm, panel_size, panel_position)
# add on top window
if not transom_thickness:
window_lining_verts = []
frame_verts = []
glass_verts = []
else:
window_lining_thickness = [lining_thickness_default] * 3
window_lining_thickness.append(transom_thickness)
window_lining_size = V(
overall_width,
lining_depth,
window_lining_height
)
window_position = V(0, 0, overall_height-window_lining_height)
frame_size = V(
door_opening_width,
frame_depth,
frame_height
)
window_lining_verts, frame_verts, glass_verts = create_bm_window(
bm,
window_lining_size,
window_lining_thickness,
lining_to_panel_offset_x,
lining_to_panel_offset_y,
frame_size,
frame_thickness,
glass_thickness,
window_position
)
lining_offset_verts = lining_verts+panel_verts+window_lining_verts+frame_verts+glass_verts
bmesh.ops.translate(bm, vec=V(0, lining_offset, 0), verts=lining_offset_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()
class BIM_OT_add_door(Operator):
bl_idname = "mesh.add_door"
bl_label = "Door"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"ERROR"}, "You need to start IFC project first to create a door.")
return {"CANCELLED"}
if context.object is not None:
spawn_location = context.object.location.copy()
context.object.select_set(False)
else:
spawn_location = bpy.context.scene.cursor.location.copy()
mesh = bpy.data.meshes.new("IfcDoor")
obj = bpy.data.objects.new("IfcDoor", mesh)
obj.location = spawn_location
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcDoor",
should_add_representation=False
)
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.bim.add_door()
return {"FINISHED"}
# UI operators
class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_door"
bl_label = "Add Door"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
if element.is_a() not in ("IfcDoor", "IfcDoorType"):
self.report({"ERROR"}, "Object has to be IfcDoor/IfcDoorType type to add a stair.")
return {"CANCELLED"}
# need to make sure all default props will have correct units
if not props.door_added_previously:
convert_property_group_from_si(props, skip_props=("is_editing", "door_type", "door_added_previously", 'convert_property_group_from_si'))
door_data = props.get_general_kwargs()
lining_props = props.get_lining_kwargs()
panel_props = props.get_panel_kwargs()
door_data["lining_properties"] = lining_props
door_data["panel_properties"] = panel_props
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("BBIM_Door", 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_Door")
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(door_data, default=list)},
)
update_door_modifier_representation(context)
return {"FINISHED"}
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cancel_editing_door"
bl_label = "Cancel editing Door"
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_Door"]["Data"])
props = obj.BIMDoorProperties
# restore previous settings since editing was canceled
for prop_name in data:
setattr(props, prop_name, data[prop_name])
update_door_modifier_representation(context)
props.is_editing = -1
return {"FINISHED"}
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.finish_editing_door"
bl_label = "Finish editing door"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
psets = ifcopenshell.util.element.get_psets(element)
pset = psets["BBIM_Door"]
door_data = props.get_general_kwargs()
lining_props = props.get_lining_kwargs()
panel_props = props.get_panel_kwargs()
door_data["lining_properties"] = lining_props
door_data["panel_properties"] = panel_props
props.is_editing = -1
update_door_modifier_representation(context)
pset = tool.Ifc.get().by_id(pset["id"])
door_data = json.dumps(door_data, default=list)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
return {"FINISHED"}
class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_door"
bl_label = "Enable Editing Door"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_psets(element)
data = json.loads(pset["BBIM_Door"]["Data"])
data.update(data.pop("lining_properties"))
data.update(data.pop("panel_properties"))
# required since we could load pset from .ifc and BIMDoorProperties won't be set
for prop_name in data:
setattr(props, prop_name, data[prop_name])
# need to make sure all props that weren't used before
# will have correct units
skip_props = ("is_editing", "door_type", "door_added_previously", 'convert_property_group_from_si')
skip_props += tuple(data.keys())
convert_property_group_from_si(props, skip_props=skip_props)
props.is_editing = 1
return {"FINISHED"}
class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_door"
bl_label = "Remove Door"
bl_options = {"REGISTER"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
obj.BIMDoorProperties.is_editing = -1
pset = ifcopenshell.util.element.get_psets(element)
pset = tool.Ifc.get().by_id(pset["BBIM_Door"]["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
props.door_added_previously = True
return {"FINISHED"}
def add_object_button(self, context):
self.layout.operator(BIM_OT_add_door.bl_idname, icon="PLUGIN")
@@ -356,13 +356,25 @@ class BIMWindowProperties(PropertyGroup):
lining_to_panel_offset_x: bpy.props.FloatProperty(name="Lining to Panel Offset X", default=0.025)
lining_to_panel_offset_y: bpy.props.FloatProperty(name="Lining to Panel Offset Y", default=0.025)
mullion_thickness: bpy.props.FloatProperty(name="Mullion Thickness", default=0.050)
first_mullion_offset: bpy.props.FloatProperty(name="First Mullion Offset", default=0.3)
second_mullion_offset: bpy.props.FloatProperty(name="Second Mullion Offset", default=0.45)
first_mullion_offset: bpy.props.FloatProperty(
name="First Mullion Offset",
description="Distance from the first lining to the first mullion center",
default=0.3)
second_mullion_offset: bpy.props.FloatProperty(
name="Second Mullion Offset",
description="Distance from the first lining to the second mullion center",
default=0.45)
transom_thickness: bpy.props.FloatProperty(name="Transom Thickness", default=0.050)
first_transom_offset: bpy.props.FloatProperty(name="First Transom Offset", default=0.3)
second_transom_offset: bpy.props.FloatProperty(name="Second Transom Offset", default=0.6)
first_transom_offset: bpy.props.FloatProperty(
name="First Transom Offset",
description="Distance from the first lining to the first transom center",
default=0.3)
second_transom_offset: bpy.props.FloatProperty(
name="Second Transom Offset",
description="Distance from the first lining to the second transom center",
default=0.6)
# panel_properties
# panel properties
frame_depth: bpy.props.FloatVectorProperty(name="Frame Depth", size=3, default=[0.035] * 3)
frame_thickness: bpy.props.FloatVectorProperty(name="Frame Thickness", size=3, default=[0.035] * 3)
@@ -417,3 +429,93 @@ class BIMWindowProperties(PropertyGroup):
"frame_depth": self.frame_depth,
"frame_thickness": self.frame_thickness,
}
class BIMDoorProperties(PropertyGroup):
door_types = (
("DOUBLE_SWING_LEFT", "DOUBLE_SWING_LEFT", ""),
("DOUBLE_SWING_RIGHT", "DOUBLE_SWING_RIGHT", ""),
)
door_added_previously: bpy.props.BoolProperty(default=False)
is_editing: bpy.props.IntProperty(default=-1)
door_type: bpy.props.EnumProperty(
name="Door Operation Type", items=door_types, default="DOUBLE_SWING_LEFT"
)
overall_height: bpy.props.FloatProperty(name="Overall Height", default=1.2)
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6)
# lining properties
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050)
lining_thickness: bpy.props.FloatProperty(name="Lining Thickness", default=0.050)
lining_offset: bpy.props.FloatProperty(name="Lining Offset", default=0.0)
lining_to_panel_offset_x: bpy.props.FloatProperty(name="Lining to Panel Offset X", default=0.025)
lining_to_panel_offset_y: bpy.props.FloatProperty(name="Lining to Panel Offset Y", default=0.025)
transom_thickness: bpy.props.FloatProperty(name="Transom Thickness", default=0.050)
transom_offset: bpy.props.FloatProperty(
name="Transom Offset",
description="Distance from the bottom door opening " \
"to the beginning of the transom (unlike windows)",
default=0.875)
casing_thickness: bpy.props.FloatProperty(name="Casing Thickness", default=0.075)
casing_depth: bpy.props.FloatProperty(name="Casing Depth", default=0.005)
threshold_thickness: bpy.props.FloatProperty(name="Threshold Thickness", default=0.025)
threshold_depth: bpy.props.FloatProperty(name="Threshold Depth", default=0.1)
threshold_offset: bpy.props.FloatProperty(name="Threshold Offset", default=0.025)
# panel properties
panel_depth: bpy.props.FloatProperty(name="Panel Depth", default=0.035)
panel_width_ratio: bpy.props.FloatProperty(
name="Panel Width Ratio",
description="Width of this panel, given as ratio " \
"relative to the total clear opening width of the door",
default=1.0, soft_min=0, soft_max=1)
frame_thickness: bpy.props.FloatProperty(name="Window Frame Thickness", default=0.035)
frame_depth: bpy.props.FloatProperty(name="Window Frame Depth", default=0.035)
def get_general_kwargs(self):
return {
"door_type": self.door_type,
"overall_height": self.overall_height,
"overall_width": self.overall_width,
}
def get_lining_kwargs(self):
kwargs = {
"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,
}
kwargs['transom_thickness'] = self.transom_thickness
if self.transom_thickness:
kwargs['transom_offset'] = self.transom_offset
if not self.lining_offset:
kwargs['casing_thickness'] = self.casing_thickness
if self.casing_thickness:
kwargs['casing_depth'] = self.casing_depth
kwargs['threshold_thickness'] = self.threshold_thickness
if self.threshold_thickness:
kwargs['threshold_depth'] = self.threshold_depth
kwargs['threshold_offset'] = self.threshold_offset
return kwargs
def get_panel_kwargs(self):
kwargs = {
"panel_depth": self.panel_depth,
"panel_width_ratio": self.panel_width_ratio
}
if self.transom_thickness:
kwargs['frame_thickness'] = self.frame_thickness
kwargs['frame_depth'] = self.frame_depth
return kwargs
@@ -19,10 +19,11 @@
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, WindowData
from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData, SverchokData, WindowData, DoorData
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.window import update_window_modifier_bmesh
from blenderbim.bim.module.model.door import update_door_modifier_bmesh
from blenderbim.bim.helper import prop_with_search
@@ -521,6 +522,89 @@ class BIM_PT_window(bpy.types.Panel):
row.operator("bim.add_window", icon="ADD", text="")
class BIM_PT_door(bpy.types.Panel):
bl_label = "IFC Door"
bl_idname = "BIM_PT_door"
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 DoorData.is_loaded:
DoorData.load()
props = context.active_object.BIMDoorProperties
if DoorData.data["parameters"]:
row = self.layout.row(align=True)
row.label(text="Door parameters", icon="OUTLINER_OB_LATTICE")
door_data = DoorData.data["parameters"]["data"]
if props.is_editing != -1:
row = self.layout.row(align=True)
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish editing")
row.operator("bim.cancel_editing_door", 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")
for prop in panel_props:
self.layout.prop(props, prop)
update_door_modifier_bmesh(context)
else:
row.operator("bim.enable_editing_door", icon="GREASEPENCIL", text="")
row.operator("bim.remove_door", icon="X", text="")
box = self.layout.box()
general_props = props.get_general_kwargs()
for prop in general_props:
prop_value = door_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 = door_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")
panel_box = self.layout.box()
for prop in panel_props:
prop_value = door_data["panel_properties"][prop]
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
row = panel_box.row(align=True)
row.label(text=f"{props.bl_rna.properties[prop].name}")
row.label(text=str(prop_value))
else:
row = self.layout.row()
row.label(text="No Door Found")
row.operator("bim.add_door", icon="ADD", text="")
class BIM_MT_model(Menu):
bl_idname = "BIM_MT_model"
bl_label = "IFC Objects"
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @Andrej730
# Copyright (C) 2023 @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
@@ -125,10 +125,11 @@ def replace_representation_for_object(ifc_file, ifc_context, obj, new_representa
)
def create_bm_window_closed_profile(bm, size: Vector, thickness: Vector, position: Vector):
"""thickness of the profile is defined as list in the following order: (LEFT, TOP, RIGHT, BOTTOM)
def create_bm_window_frame(bm, size: Vector, thickness: Vector, position: Vector = V(0,0,0)):
"""`thickness` of the profile is defined as list in the following order:
`(LEFT, TOP, RIGHT, BOTTOM)`
thickness can be also defined just as 1 float value.
`thickness` can be also defined just as 1 float value.
"""
if not isinstance(thickness, collections.abc.Iterable):
@@ -189,6 +190,52 @@ def create_bm_window_closed_profile(bm, size: Vector, thickness: Vector, positio
return new_verts + translate_verts
def create_bm_box(bm, size:Vector=V(1,1,1).freeze(), position:Vector=V(0,0,0).freeze()):
"""create a box of `size`, position box first vertex at `position`"""
box_verts = bmesh.ops.create_cube(bm, size=1)['verts']
bmesh.ops.translate(bm, vec=-box_verts[0].co, verts=box_verts)
bmesh.ops.scale(bm, vec=size, verts=box_verts)
bmesh.ops.translate(bm, vec=position, verts=box_verts)
return box_verts
def create_bm_window(bm,
lining_size: Vector,
lining_thickness,
lining_to_panel_offset_x,
lining_to_panel_offset_y_full,
frame_size,
frame_thickness,
glass_thickness,
position: Vector):
# window lining
window_lining_verts = create_bm_window_frame(bm, lining_size, lining_thickness)
# window frame
frame_position = V(
lining_to_panel_offset_x,
lining_to_panel_offset_y_full,
lining_to_panel_offset_x
)
frame_verts = create_bm_window_frame(bm, frame_size, frame_thickness, frame_position)
# window glass
glass_size = frame_size - V(frame_thickness*2, 0, frame_thickness*2)
glass_size.y = glass_thickness
glass_position = frame_position + V(
frame_thickness,
frame_size.y / 2 - glass_thickness / 2,
frame_thickness
)
glass_verts = create_bm_box(bm, glass_size, glass_position)
translated_verts = window_lining_verts + frame_verts + glass_verts
bmesh.ops.translate(bm, vec=position, verts=translated_verts)
return (window_lining_verts, frame_verts, glass_verts)
def update_window_modifier_bmesh(context):
obj = context.object
props = obj.BIMWindowProperties
@@ -201,7 +248,9 @@ def update_window_modifier_bmesh(context):
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
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion
lining_thickness = props.lining_thickness * si_conversion
lining_offset = props.lining_offset
mullion_thickness = props.mullion_thickness * si_conversion / 2
first_mullion_offset = props.first_mullion_offset * si_conversion
@@ -251,8 +300,8 @@ def update_window_modifier_bmesh(context):
frame_depth = props.frame_depth[panel_i] * si_conversion
frame_thickness = props.frame_thickness[panel_i] * si_conversion
# create lining
lining_size = V(
# add window
window_lining_size = V(
panel_width,
lining_depth,
panel_height,
@@ -260,59 +309,43 @@ def update_window_modifier_bmesh(context):
# calculate lining thickness
# taking into account mullions and transoms
thickness = [lining_thickness] * 4
window_lining_thickness = [lining_thickness] * 4
# mullion thickness
if unique_cols > 1:
if column_i != 0:
thickness[0] = mullion_thickness # left column
window_lining_thickness[0] = mullion_thickness # left column
if column_i != unique_cols - 1:
thickness[2] = mullion_thickness # right column
window_lining_thickness[2] = mullion_thickness # right column
# transom thickness
if unique_rows_in_col[column_i] > 1:
if row_i != 0:
thickness[3] = transom_thickness # bottom row
window_lining_thickness[3] = transom_thickness # bottom row
if row_i != unique_rows_in_col[column_i] - 1:
thickness[1] = transom_thickness # top row
window_lining_thickness[1] = transom_thickness # top row
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_size = window_lining_size.copy()
frame_size.y = frame_depth
frame_size = frame_size - V(lining_to_panel_offset_x * 2, 0, lining_to_panel_offset_x * 2)
window_position = V(accumulated_width, 0, accumulated_height[column_i])
lining_verts, panel_verts, glass_verts = create_bm_window(
bm,
window_lining_size,
window_lining_thickness,
lining_to_panel_offset_x,
(lining_depth - frame_depth) + lining_to_panel_offset_y,
frame_size,
frame_thickness,
glass_thickness,
window_position
)
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])
bmesh.ops.translate(bm, vec=accumulated_offset, verts=lining_verts + panel_verts + glass_verts)
built_panels.append(panel_i)
accumulated_height[column_i] += panel_height
accumulated_width += panel_width
bmesh.ops.translate(bm, vec=V(0, props.lining_offset * si_conversion, 0), verts=bm.verts)
bmesh.ops.translate(bm, vec=V(0, lining_offset, 0), verts=bm.verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
if bpy.context.object.mode == "EDIT":
@@ -0,0 +1,297 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 @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 ifcopenshell.util.shape_builder import ShapeBuilder, V
from ifcopenshell.api.geometry.add_window_representation import create_ifc_window
from mathutils import Vector
import collections
def create_ifc_door_lining_items(builder: ShapeBuilder, size: Vector, thickness: Vector, position:Vector=V(0,0,0).freeze()):
"""`thickness` of the profile is defined as list in the following order: `(SIDE, TOP)`
`thickness` can be also defined just as 1 float value.
"""
if not isinstance(thickness, collections.abc.Iterable):
thickness = [thickness] * 2
th_side, th_up = thickness
left_lining = builder.rectangle(V(th_side, 0, size.z))
right_lining = builder.translate(left_lining, V(size.x-th_side, 0, 0), create_copy=True)
top_lining = builder.rectangle(V(size.x, 0, th_up), V(0, 0, size.z-th_up))
items = [left_lining, right_lining, top_lining]
items = [builder.extrude(l, size.y, extrusion_vector=V(0,1,0)) for l in items]
builder.translate(items, position)
return items
def create_ifc_box(builder: ShapeBuilder, size: Vector, position:Vector=V(0,0,0).freeze()):
rect = builder.rectangle(size.xy)
box = builder.extrude(rect, size.z, position=position, extrusion_vector=V(0,0,1))
return box
class Usecase:
def __init__(self, file, **settings):
"""units in settings expected to be in ifc project units"""
self.file = file
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoor.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorLiningProperties.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorPanelProperties.htm
self.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(self.file)}
# TODO: remove upper window from default props after tests
# TODO: check default values with blender props
self.settings.update(
{
"context": None, # IfcGeometricRepresentationContext
"overall_height": self.convert_si_to_unit(0.9),
"overall_width": self.convert_si_to_unit(0.6),
# DOUBLE_DOOR_DOUBLE_SWING, DOUBLE_DOOR_FOLDING, DOUBLE_DOOR_LIFTING_VERTICAL,
# DOUBLE_DOOR_SINGLE_SWING, DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_LEFT,
# DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_RIGHT, DOUBLE_DOOR_SLIDING,
# DOUBLE_SWING_LEFT, DOUBLE_SWING_RIGHT, FOLDING_TO_LEFT,
# FOLDING_TO_RIGHT, LIFTING_HORIZONTAL, LIFTING_VERTICAL_LEFT,
# LIFTING_VERTICAL_RIGHT, REVOLVING, REVOLVING_VERTICAL,
# ROLLINGUP, SINGLE_SWING_LEFT, SINGLE_SWING_RIGHT, SLIDING_TO_LEFT,
# SLIDING_TO_RIGHT, SWING_FIXED_LEFT, SWING_FIXED_RIGHT
# TODO: take into account door types
"operation_type": "SINGLE_SWING_LEFT", # door type
"lining_properties": {
"LiningDepth": self.convert_si_to_unit(0.050),
"LiningThickness": self.convert_si_to_unit(0.050),
# offset from the outer side of the wall (by Y-axis)
"LiningOffset": self.convert_si_to_unit(0.050),
# offset from the wall
"LiningToPanelOffsetX": self.convert_si_to_unit(0.025),
# offset from the elevation view rectangle (unlike windows)
"LiningToPanelOffsetY": self.convert_si_to_unit(0.025),
# Casing cover wall faces around the opening
# on the left, right and upper sides
# Casing should be either on both sides of the wall or no casing
# If `LiningOffset` is present then therefore casing is not possible on outer wall
# therefore there will be no casing on inner wall either
"CasingDepth": self.convert_si_to_unit(0.025),
"CasingThickness": self.convert_si_to_unit(0.075), # by Z-axis
# TODO: link to wall thickness?
# Threshold covers the bottom side of the opening
"ThresholdDepth": self.convert_si_to_unit(0.025),
"ThresholdThickness": self.convert_si_to_unit(0.025), # by Z-axis
# offset to X-axis
# TODO: offset goes by Z-axis?
"ThresholdOffset": self.convert_si_to_unit(0.025),
# transom - vertical distance between door and window panels
"TransomThickness": self.convert_si_to_unit(0.050),
# TransomOffset - distance from the bottom door opening
# to the beginning of the transom
# unlike windows TransomOffset which goes to the center of the transom
"TransomOffset": self.convert_si_to_unit(0.6),
"ShapeAspectStyle": None, # DEPRECATED
},
"panel_properties": {
"PanelDepth": self.convert_si_to_unit(0.030), # by Y
"PanelWidth": 1.0, # as ratio to the clear door opening
"FrameDepth": self.convert_si_to_unit(0.035), # by Y
"FrameThickness": self.convert_si_to_unit(0.035), # by X
# LEFT, MIDDLE, RIGHT, NOTDEFINED
"PanelPosition": ..., # NEVER USED
# defines the basic ways to describe how door panels operate
# basically how it opens
"PanelOperation": None, # NEVER USED
"ShapeAspectStyle": None, # DEPRECATED
},
}
)
for key, value in settings.items():
self.settings[key] = value
def execute(self):
builder = ShapeBuilder(self.file)
overall_height = self.settings["overall_height"]
overall_width = self.settings["overall_width"]
# TODO: elevation view representation
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
# TODO: implement 2d representation
panel_props = self.settings["panel_properties"]
lining_props = self.settings["lining_properties"]
# lining params
lining_depth = lining_props['LiningDepth']
lining_thickness_default = lining_props['LiningThickness']
lining_offset = lining_props['LiningOffset']
lining_to_panel_offset_x = lining_props['LiningToPanelOffsetX']
lining_to_panel_offset_y_full = lining_props['LiningToPanelOffsetY']
transom_thickness = lining_props['TransomThickness'] / 2
transfom_offset = lining_props['TransomOffset']
if transom_thickness == 0:
transfom_offset = 0
window_lining_height = overall_height - transfom_offset - transom_thickness
top_lining_thickness = transom_thickness or lining_thickness_default
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
door_opening_width = (overall_width - lining_to_panel_offset_x * 2)
threshold_thickness = lining_props['ThresholdThickness']
threshold_depth = lining_props['ThresholdDepth']
threshold_offset = lining_props['ThresholdOffset']
threshold_width = overall_width - lining_thickness_default * 2
casing_thickness = lining_props['CasingThickness']
casing_depth = lining_props['CasingDepth']
# panel params
panel_depth = panel_props['PanelDepth']
panel_width = door_opening_width * panel_props['PanelWidth']
frame_depth = panel_props['FrameDepth']
frame_thickness = panel_props['FrameThickness']
frame_height = window_lining_height - lining_to_panel_offset_x * 2
glass_thickness = self.convert_si_to_unit(0.01)
if transfom_offset:
panel_height = transfom_offset + transom_thickness - lining_to_panel_offset_x - threshold_thickness
lining_height = transfom_offset + transom_thickness
else:
panel_height = overall_height - lining_to_panel_offset_x - threshold_thickness
lining_height = overall_height
# add lining
lining_size = V(overall_width, lining_depth, lining_height)
lining_thickness = [lining_thickness_default, top_lining_thickness]
lining_items = []
main_lining_size = lining_size
# need to check offsets to decide whether lining should be rectangle
# or L shaped
l_shape_check = lining_to_panel_offset_y_full < lining_size.y \
and any(lining_to_panel_offset_x < th for th in lining_thickness)
if l_shape_check:
main_lining_size = lining_size.copy()
main_lining_size.y = lining_to_panel_offset_y_full
second_lining_size = lining_size.copy()
second_lining_size.y = lining_size.y - lining_to_panel_offset_y_full
second_lining_position = V(0, lining_to_panel_offset_y_full, 0)
second_lining_thickness = [min(th, lining_to_panel_offset_x) for th in lining_thickness]
second_lining = create_ifc_door_lining_items(builder,
second_lining_size,
second_lining_thickness,
second_lining_position)
lining_items.extend(second_lining)
main_lining = create_ifc_door_lining_items(builder, main_lining_size, lining_thickness)
lining_items.extend(main_lining)
# add threshold
if not threshold_thickness:
threshold_items = []
else:
threshold_size = V(threshold_width, threshold_depth, threshold_thickness)
threshold_position = V(lining_thickness_default, threshold_offset, 0)
threshold_items = [create_ifc_box(builder, threshold_size, threshold_position)]
# add casings
casing_items = []
if not lining_offset and casing_thickness:
casing_wall_overlap = max(casing_thickness - lining_thickness_default, 0)
casing_size = V(overall_width + casing_wall_overlap*2, casing_depth, overall_height+casing_wall_overlap)
casing_position = V(-casing_wall_overlap, -casing_depth, 0)
outer_casing_items = create_ifc_door_lining_items(builder, casing_size, casing_thickness, casing_position)
casing_items.extend(outer_casing_items)
inner_casing_thickness = [
casing_thickness-panel_lining_overlap_x,
casing_thickness-panel_top_lining_overlap_x
]
inner_casing_position = V(-casing_wall_overlap, lining_depth, 0)
inner_casing_items = create_ifc_door_lining_items(builder, casing_size, inner_casing_thickness, inner_casing_position)
casing_items.extend(inner_casing_items)
# add door panel
panel_size = V(
panel_width,
panel_depth,
panel_height
)
panel_position = V(
lining_to_panel_offset_x,
lining_to_panel_offset_y_full,
threshold_thickness
)
panel_items = [create_ifc_box(builder, panel_size, panel_position)]
# add on top window
if not transom_thickness:
window_lining_items = []
frame_items = []
glass_items = []
else:
window_lining_thickness = [lining_thickness_default] * 3
window_lining_thickness.append(transom_thickness)
window_lining_size = V(
overall_width,
lining_depth,
window_lining_height
)
window_position = V(0, 0, overall_height-window_lining_height)
frame_size = V(
door_opening_width,
frame_depth,
frame_height
)
window_lining_items, frame_items, glass_items = create_ifc_window(
builder,
window_lining_size,
window_lining_thickness,
lining_to_panel_offset_x,
lining_to_panel_offset_y_full,
frame_size,
frame_thickness,
glass_thickness,
window_position
)
lining_offset_items = lining_items+panel_items+window_lining_items+frame_items+glass_items
builder.translate(lining_offset_items, V(0, lining_offset, 0))
ouput_items = lining_offset_items + threshold_items + casing_items
print(len(ouput_items), 'items ready')
representation = builder.get_representation(self.settings["context"], ouput_items)
return representation
def convert_si_to_unit(self, value):
return value / self.settings["unit_scale"]
@@ -1,5 +1,5 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 @Andrej730
# Copyright (C) 2023 @Andrej730
#
# This file is part of IfcOpenShell.
#
@@ -17,9 +17,10 @@
# 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 itertools import chain
from mathutils import Vector
import collections
# SCHEMAS describe panels setup
@@ -40,6 +41,95 @@ DEFAULT_PANEL_SCHEMAS = {
"TRIPLE_PANEL_VERTICAL": [[0, 1, 2]],
}
def create_ifc_window_frame_simple(
builder,
size: Vector,
thickness: Vector,
position: Vector = V(0,0,0)):
"""`thickness` of the profile is defined as list in the following order:
`(LEFT, TOP, RIGHT, BOTTOM)`
`thickness` can be also defined just as 1 float value.
"""
if not isinstance(thickness, collections.abc.Iterable):
thickness = [thickness] * 4
th_left, th_up, th_right, th_bottom = thickness
panel_rect = builder.rectangle(size=size*V(1,0,1))
inner_rect_size = size - V(th_left+th_right, 0, th_bottom+th_up)
inner_rect = builder.rectangle(
size=inner_rect_size*V(1, 0, 1),
position=V(th_left, 0, th_bottom)
)
panel_profile = builder.profile(panel_rect, inner_curves=inner_rect)
panel_extruded = builder.extrude(
panel_profile,
size.y,
extrusion_vector=V(0, 1, 0),
position=position
)
return panel_extruded
def create_ifc_window(
builder,
lining_size: Vector,
lining_thickness,
lining_to_panel_offset_x,
lining_to_panel_offset_y_full,
frame_size,
frame_thickness,
glass_thickness,
position: Vector):
lining_items = []
main_lining_size = lining_size
# need to check offsets to decide whether lining should be rectangle
# or L shaped
l_shape_check = lining_to_panel_offset_y_full < lining_size.y \
and any(lining_to_panel_offset_x < th for th in lining_thickness)
if l_shape_check:
main_lining_size = lining_size.copy()
main_lining_size.y = lining_to_panel_offset_y_full
second_lining_size = lining_size.copy()
second_lining_size.y = lining_size.y - lining_to_panel_offset_y_full
second_lining_position = V(0, lining_to_panel_offset_y_full, 0)
second_lining_thickness = [min(th, lining_to_panel_offset_x) for th in lining_thickness]
second_lining = create_ifc_window_frame_simple(builder,
second_lining_size,
second_lining_thickness,
second_lining_position)
lining_items.append(second_lining)
main_lining = create_ifc_window_frame_simple(builder, main_lining_size, lining_thickness)
lining_items.append(main_lining)
frame_position = V(
lining_to_panel_offset_x,
lining_to_panel_offset_y_full,
lining_to_panel_offset_x
)
frame_extruded = create_ifc_window_frame_simple(builder, frame_size, frame_thickness, frame_position)
glass_position = frame_position + V(0, frame_size.y / 2 - glass_thickness / 2, 0)
glass_rect = builder.deep_copy(frame_extruded.SweptArea.InnerCurves[0])
glass = builder.extrude(
glass_rect, glass_thickness, extrusion_vector=V(0, 1, 0), position=glass_position
)
output_items = [lining_items, [frame_extruded], [glass]]
builder.translate(chain(*output_items), position)
return output_items
class Usecase:
def __init__(self, file, **settings):
@@ -63,7 +153,10 @@ class Usecase:
"LiningDepth": self.convert_si_to_unit(0.050),
"LiningThickness": self.convert_si_to_unit(0.050),
"LiningOffset": self.convert_si_to_unit(0.050), # offset to the wall
# offset from the wall
"LiningToPanelOffsetX": self.convert_si_to_unit(0.025),
# offset from the lining
# full offset from Y axis = (lining_depth - frame_depth) + lining_to_panel_offset_y
"LiningToPanelOffsetY": self.convert_si_to_unit(0.025),
# applies to DoublePanelVertical, TriplePanelBottom, TriplePanelTop,
# TriplePanelLeft, TriplePanelRight
@@ -72,7 +165,7 @@ class Usecase:
# distance from the first lining to the mullion center
"FirstMullionOffset": self.convert_si_to_unit(0.3),
# applies to TriplePanelVertical
# distance from the first lining to the second mullion
# distance from the first lining to the second mullion center
"SecondMullionOffset": self.convert_si_to_unit(0.45),
# applies to DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop,
# TriplePanelLeft, TriplePanelRight
@@ -118,19 +211,21 @@ class Usecase:
built_panels = []
window_items = []
lining_thickness = self.settings["lining_properties"]["LiningThickness"]
lining_depth = self.settings["lining_properties"]["LiningDepth"]
lining_offset = self.settings["lining_properties"]["LiningOffset"]
lining_panel_offset_x = self.settings["lining_properties"]["LiningToPanelOffsetX"]
lining_panel_offset_y = self.settings["lining_properties"]["LiningToPanelOffsetY"]
glass_thickness = self.convert_si_to_unit(0.01)
mullion_thickness = self.settings["lining_properties"]["MullionThickness"] / 2
first_mullion_offset = self.settings["lining_properties"]["FirstMullionOffset"]
second_mullion_offset = self.settings["lining_properties"]["SecondMullionOffset"]
transom_thickness = self.settings["lining_properties"]["TransomThickness"] / 2
first_transom_offset = self.settings["lining_properties"]["FirstTransomOffset"]
second_transom_offset = self.settings["lining_properties"]["SecondTransomOffset"]
lining_props = self.settings["lining_properties"]
lining_thickness = lining_props["LiningThickness"]
lining_depth = lining_props["LiningDepth"]
lining_offset = lining_props["LiningOffset"]
lining_to_panel_offset_x = lining_props["LiningToPanelOffsetX"]
lining_to_panel_offset_y = lining_props["LiningToPanelOffsetY"]
mullion_thickness = lining_props["MullionThickness"] / 2
first_mullion_offset = lining_props["FirstMullionOffset"]
second_mullion_offset = lining_props["SecondMullionOffset"]
transom_thickness = lining_props["TransomThickness"] / 2
first_transom_offset = lining_props["FirstTransomOffset"]
second_transom_offset = lining_props["SecondTransomOffset"]
glass_thickness = self.convert_si_to_unit(0.01)
panel_schema = list(reversed(panel_schema))
@@ -146,193 +241,75 @@ class Usecase:
# calculate current panel dimensions
if unique_cols > 1:
if column_i == 0:
panel_width = first_mullion_offset
lining_width = first_mullion_offset
elif column_i == unique_cols - 1:
panel_width = overall_width - accumulated_width
lining_width = overall_width - accumulated_width
else:
panel_width = second_mullion_offset - accumulated_width
lining_width = second_mullion_offset - accumulated_width
else:
panel_width = overall_width
lining_width = overall_width
if unique_rows_in_col[column_i] > 1:
if row_i == 0:
panel_height = first_transom_offset
lining_height = first_transom_offset
elif row_i == unique_rows_in_col[column_i] - 1:
panel_height = overall_height - accumulated_height[column_i]
lining_height = overall_height - accumulated_height[column_i]
else:
panel_height = second_transom_offset - accumulated_height[column_i]
lining_height = second_transom_offset - accumulated_height[column_i]
else:
panel_height = overall_height
lining_height = overall_height
if panel_i in built_panels:
accumulated_height[column_i] += panel_height
accumulated_width += panel_width
accumulated_height[column_i] += lining_height
accumulated_width += lining_width
continue
cur_panel = panels[panel_i]
panel_depth = cur_panel["FrameDepth"]
panel_thickness = cur_panel["FrameThickness"]
frame_depth = cur_panel["FrameDepth"]
frame_thickness = cur_panel["FrameThickness"]
current_items = []
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 is calculated on panel level because
# panel depth is used
lining_items_vertical_left = []
lining_items = []
frame_width = lining_width - lining_to_panel_offset_x * 2
frame_height = lining_height - lining_to_panel_offset_x * 2
# calculate lining thickness
# taking into account mullions and transoms
thickness = [lining_thickness] * 4
# lining is calculated on panel level because
# panel depth is used
# also taking into account mullions and transoms
window_lining_thickness = [lining_thickness] * 4
# mullion thickness
if unique_cols > 1:
if column_i != 0:
thickness[0] = mullion_thickness # left column
window_lining_thickness[0] = mullion_thickness # left column
if column_i != unique_cols - 1:
thickness[2] = mullion_thickness # right column
window_lining_thickness[2] = mullion_thickness # right column
# transom thickness
if unique_rows_in_col[column_i] > 1:
if row_i != 0:
thickness[3] = transom_thickness # bottom row
window_lining_thickness[3] = transom_thickness # bottom row
if row_i != unique_rows_in_col[column_i] - 1:
thickness[1] = transom_thickness # top row
window_lining_thickness[1] = transom_thickness # top row
window_lining_size = V(lining_width, lining_depth, lining_height)
frame_size = V(frame_width, frame_depth, frame_height)
window_panel_position = V(accumulated_width, 0, accumulated_height[column_i])
def get_lining_rectangle(current_lining_thickness):
lining_rectangle = builder.rectangle(size=V(current_lining_thickness, lining_depth))
return lining_rectangle
def get_lining_polyline(current_lining_thickness):
# need to check offsets to decide whether lining should be rectangle
# or L shaped
if lining_panel_offset_x >= current_lining_thickness or lining_panel_offset_y >= lining_depth:
lining_polyline = get_lining_rectangle(current_lining_thickness)
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(current_lining_thickness, lining_depth - (panel_depth - lining_panel_offset_y)),
V(current_lining_thickness, 0),
]
lining_polyline = builder.polyline(lining_points, closed=True)
return lining_polyline
def create_lining_vertical(current_lining_thickness):
current_vertical_lining_items = []
lining_vertical_polyline = get_lining_polyline(current_lining_thickness)
lining_vertical_height = panel_height - lining_panel_offset_x * 2
extrusion_position = V(0, 0, lining_panel_offset_x)
lining_vertical_extruded = builder.extrude(
lining_vertical_polyline, lining_vertical_height, position=extrusion_position
)
current_vertical_lining_items.append(lining_vertical_extruded)
# 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(
get_lining_rectangle(current_lining_thickness), lining_panel_offset_x
)
current_vertical_lining_items.append(lining_vertical_addition)
current_vertical_lining_items.append(
builder.translate(
lining_vertical_addition,
V(0, 0, panel_height - lining_panel_offset_x),
create_copy=True,
)
)
return current_vertical_lining_items
# vertical lining
lining_items_vertical_left = create_lining_vertical(thickness[0])
lining_items_vertical_right = create_lining_vertical(thickness[2])
lining_items_vertical_right = builder.mirror(
lining_items_vertical_right, mirror_point=V(panel_width / 2, 0), mirror_axes=V(1, 0)
# create window panel
current_window_items = create_ifc_window(
builder,
window_lining_size,
window_lining_thickness,
lining_to_panel_offset_x,
(lining_depth - frame_depth) + lining_to_panel_offset_y,
frame_size,
frame_thickness,
glass_thickness,
window_panel_position
)
lining_items.extend(lining_items_vertical_left)
lining_items.extend(lining_items_vertical_right)
# horizontal lining
def create_horizontal_lining(current_lining_thickness, mirror_point=None):
lining_horizontal_polyline = get_lining_polyline(current_lining_thickness)
if mirror_point:
lining_horizontal_polyline = builder.mirror(
lining_horizontal_polyline,
mirror_axes=V(1, 0),
mirror_point=mirror_point
)
builder.translate(lining_horizontal_polyline, -mirror_point)
lining_horizontal_extruded = builder.extrude(
lining_horizontal_polyline,
magnitude=panel_width - (thickness[0] + thickness[2]),
extrusion_vector=V(0, 0, -1),
position_z_axis=V(-1, 0, 0),
position_x_axis=V(0, 0, 1),
)
return lining_horizontal_extruded
lining_horizontal_bottom = create_horizontal_lining(thickness[3])
builder.translate(lining_horizontal_bottom, V(thickness[0], 0, 0))
lining_horizontal_top = create_horizontal_lining(thickness[1], mirror_point=V(thickness[1], 0))
builder.translate(lining_horizontal_top, V(thickness[0], 0, panel_height - thickness[1]))
# 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_items.extend([lining_horizontal_bottom, lining_horizontal_top])
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])
builder.translate(current_items, accumulated_offset)
built_panels.append(panel_i)
window_items.extend(current_items)
window_items.extend(chain(*current_window_items))
accumulated_height[column_i] += panel_height
accumulated_width += panel_width
accumulated_height[column_i] += lining_height
accumulated_width += lining_width
builder.translate(window_items, V(0, lining_offset, 0)) # wall offset
representation = builder.get_representation(self.settings["context"], window_items)