mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-15 18:14:08 +00:00
448 lines
17 KiB
Python
448 lines
17 KiB
Python
# 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
|
|
import collections
|
|
|
|
|
|
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,
|
|
"MullionThickness": props.mullion_thickness,
|
|
"FirstMullionOffset": props.first_mullion_offset,
|
|
"SecondMullionOffset": props.second_mullion_offset,
|
|
"TransomThickness": props.transom_thickness,
|
|
"FirstTransomOffset": props.first_transom_offset,
|
|
"SecondTransomOffset": props.second_transom_offset,
|
|
},
|
|
"panel_properties": [],
|
|
}
|
|
number_of_panels, panels_data = 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],
|
|
}
|
|
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.Ifc,
|
|
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: Vector, position: Vector):
|
|
"""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
|
|
|
|
width, depth, height = size
|
|
|
|
verts = [
|
|
(0, [th_left, 0.0, th_bottom]),
|
|
(1, [width - th_right, 0.0, th_bottom]),
|
|
(2, [th_left, 0.0, height - th_up]),
|
|
(3, [width - th_right, 0.0, height - th_up]),
|
|
(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, (5, 7, 3, 2)),
|
|
(1, (2, 0, 4, 5)),
|
|
(2, (6, 4, 0, 1)),
|
|
(3, (6, 1, 3, 7)),
|
|
]
|
|
|
|
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_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
|
|
lining_thickness = props.lining_thickness * si_conversion
|
|
|
|
mullion_thickness = props.mullion_thickness * si_conversion / 2
|
|
first_mullion_offset = props.first_mullion_offset * si_conversion
|
|
second_mullion_offset = props.second_mullion_offset * si_conversion
|
|
transom_thickness = props.transom_thickness * si_conversion / 2
|
|
first_transom_offset = props.first_transom_offset * si_conversion
|
|
second_transom_offset = props.second_transom_offset * si_conversion
|
|
|
|
glass_thickness = 10 * si_conversion
|
|
|
|
bm = bmesh.new()
|
|
panel_schema = list(reversed(panel_schema))
|
|
|
|
# TODO: need more readable way to define panel width and height
|
|
unique_rows_in_col = [len(set(row[column_i] for row in panel_schema)) for column_i in range(len(panel_schema[0]))]
|
|
for row_i, panel_row in enumerate(panel_schema):
|
|
accumulated_width = 0
|
|
unique_cols = len(set(panel_row))
|
|
|
|
for column_i, panel_i in enumerate(panel_row):
|
|
# calculate current panel dimensions
|
|
if unique_cols > 1:
|
|
if column_i == 0:
|
|
panel_width = first_mullion_offset
|
|
elif column_i == unique_cols - 1:
|
|
panel_width = overall_width - accumulated_width
|
|
else:
|
|
panel_width = second_mullion_offset - accumulated_width
|
|
else:
|
|
panel_width = overall_width
|
|
|
|
if unique_rows_in_col[column_i] > 1:
|
|
if row_i == 0:
|
|
panel_height = first_transom_offset
|
|
elif row_i == unique_rows_in_col[column_i] - 1:
|
|
panel_height = overall_height - accumulated_height[column_i]
|
|
else:
|
|
panel_height = second_transom_offset - accumulated_height[column_i]
|
|
else:
|
|
panel_height = overall_height
|
|
|
|
if panel_i in built_panels:
|
|
accumulated_height[column_i] += panel_height
|
|
accumulated_width += panel_width
|
|
continue
|
|
|
|
frame_depth = props.frame_depth[panel_i] * si_conversion
|
|
frame_thickness = props.frame_thickness[panel_i] * si_conversion
|
|
|
|
# create lining
|
|
lining_size = V(
|
|
panel_width,
|
|
lining_depth,
|
|
panel_height,
|
|
)
|
|
|
|
# calculate lining thickness
|
|
# taking into account mullions and transoms
|
|
thickness = [lining_thickness] * 4
|
|
# mullion thickness
|
|
if unique_cols > 1:
|
|
if column_i != 0:
|
|
thickness[0] = mullion_thickness # left column
|
|
if column_i != unique_cols - 1:
|
|
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
|
|
if row_i != unique_rows_in_col[column_i] - 1:
|
|
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_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])
|
|
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.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"}
|