mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Squashed commit of the following:
commit7fb4cdaa16Author: Gorgious <nathan.hild@gmail.com> Date: Mon Oct 31 09:35:06 2022 +0100 Fix #2546 : Editing a material now automatically ends the edition of its material set item if necessary commit427411e758Author: Dion Moult <dion@thinkmoult.com> Date: Mon Oct 31 19:04:57 2022 +1100 You can now edit arbitrary profiles in the profile manager commitcc056d5d53Author: Gorgious <nathan.hild@gmail.com> Date: Mon Oct 31 08:52:11 2022 +0100 Fix #2553 : Right click documentation is now only available for Blender 3.3+ Also fix an error in the console when right clicking a button commit31600033bfAuthor: ArturTomczak <artomczak@gmail.com> Date: Sun Oct 30 23:16:08 2022 +0100 add BCF reporter to IDS (#2552) * grammar * reason not always a list * extend readme with instruction * remove microseconds from date * add BCF reporter commit0338825175Author: Dion Moult <dion@thinkmoult.com> Date: Sun Oct 30 23:26:59 2022 +1100 You can now add new profile definitions in the profile manager commitd9f8aa4e7bAuthor: Andrej730 <azhilenkov@gmail.com> Date: Sun Oct 30 11:29:42 2022 +0600 Added ifc types schema for Ifc2x3 and Ifc4 Types schema located in separate json file, it's structure: { ifc_type_name: {"description": type_description, "spec_url": type_spec_url} } I've also added new API function to get type data - `get_type_doc(version, ifc_type)` commit6803788676Author: Dion Moult <dion@thinkmoult.com> Date: Sun Oct 30 11:02:18 2022 +1100 Load thumbnails in a paginated manner to allow to scale to larger models commitd421cc0bbbAuthor: Massimo Fabbro <79401028+maxfb87@users.noreply.github.com> Date: Sun Oct 30 00:07:56 2022 +0200 No mapped quantities in qto calculator has None value (#2549) commitbf57c8b1bfAuthor: Dion Moult <dion@thinkmoult.com> Date: Sat Oct 29 23:45:04 2022 +1100 Refactor profile module to use data class commit9a25db6584Author: Andrej730 <azhilenkov@gmail.com> Date: Sat Oct 29 18:31:40 2022 +0600 get_attribute_doc get_entity_doc now use recursive method by default commit17ccd0adf7Author: Andrej730 <azhilenkov@gmail.com> Date: Sat Oct 29 18:27:51 2022 +0600 Simplified getting entity parent information for schema api Removed unnecssary parent information from json schema - now API is getting it from ifcopenshell schema. commit937f39c898Author: Andrej730 <azhilenkov@gmail.com> Date: Sat Oct 29 17:35:43 2022 +0600 Added information about parent entity to schema Added information about parent entity to schema (it's located in "parent_entity" entity parameter inside schema), both for Ifc2x3 and Ifc4. Modified get_entity_doc and get_attribute_doc - now you can use additional optional parameter `recursive=True` with those functions - then parent entities attributes will be included to the list of entity's attributes. For example IfcWindow will also have attributes from IfcBuildingElement, IfcElement, IfcProduct etc... commit0b5ccd88a5Author: Andrej730 <azhilenkov@gmail.com> Date: Sat Oct 29 16:31:54 2022 +0600 Added descriptions for psets and qsets to ifc2x3, ifc4 schema Example of getting pset description: `get_property_set_doc("IFC4", "Pset_ZoneCommon")['description']` commitebf3cd8909Author: Andrej730 <azhilenkov@gmail.com> Date: Sat Oct 29 10:32:26 2022 +0600 Added IFC libraries for AU and EU profiles Also added script that was used to generate those libraries (the script is using data from https://github.com/boltsparts/BOLTS). Types of steel profiles included: I profiles, Z profiles, C profiles (known as U profiles in IFC), L profiles (both equal and unequal), hollow profiles (circle, square and rectangular). commit08afec3461Author: Dion Moult <dion@thinkmoult.com> Date: Fri Oct 28 22:32:15 2022 +1100 Support material layer set usage negative direction sense for parametric walls commit4408a23873Author: Dion Moult <dion@thinkmoult.com> Date: Fri Oct 28 22:30:43 2022 +1100 Merge duplicate types IfcPatch recipe can now merge based on any attribute
This commit is contained in:
@@ -223,10 +223,10 @@ class BcfXml:
|
||||
|
||||
def edit_topic(self, topic):
|
||||
if not topic.creation_date:
|
||||
topic.creation_date = datetime.utcnow().isoformat()
|
||||
topic.creation_date = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
topic.creation_author = self.author
|
||||
else:
|
||||
topic.modified_date = datetime.utcnow().isoformat()
|
||||
topic.modified_date = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
topic.modified_author = self.author
|
||||
|
||||
self.document = minidom.Document()
|
||||
@@ -345,10 +345,10 @@ class BcfXml:
|
||||
|
||||
def edit_comment(self, comment, topic):
|
||||
if not comment.date:
|
||||
comment.date = datetime.utcnow().isoformat()
|
||||
comment.date = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
comment.author = self.author
|
||||
else:
|
||||
comment.modified_date = datetime.utcnow().isoformat()
|
||||
comment.modified_date = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
comment.modified_author = self.author
|
||||
self.edit_topic(topic)
|
||||
|
||||
@@ -595,7 +595,7 @@ class BcfXml:
|
||||
copyfile(header_file.reference, os.path.join(topic_filepath, header_file.filename))
|
||||
header_file.reference = header_file.filename
|
||||
header_file.is_external = False
|
||||
header_file.date = datetime.utcnow().isoformat()
|
||||
header_file.date = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
if not topic.header:
|
||||
topic.header = bcf.v2.data.Header()
|
||||
topic.header.files.append(header_file)
|
||||
|
||||
@@ -160,7 +160,8 @@ def register():
|
||||
bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.SCENE_PT_unit.append(ui.ifc_units)
|
||||
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
|
||||
if hasattr(bpy.types, "UI_MT_button_context_menu"):
|
||||
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
|
||||
|
||||
for mod in modules.values():
|
||||
mod.register()
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -26,6 +26,7 @@ from blenderbim.bim.module.drawing.prop import RasterStyleProperty
|
||||
from bpy.app.handlers import persistent
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
from ifcopenshell.api.type.data import Data as TypeData
|
||||
|
||||
@@ -301,3 +302,4 @@ def setDefaultProperties(scene):
|
||||
drawing_style.name = "Blender Default"
|
||||
drawing_style.render_type = "DEFAULT"
|
||||
bpy.ops.bim.save_drawing_style(index="2")
|
||||
AuthoringData.type_thumbnails = {}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
@@ -51,9 +52,14 @@ class CadTool(WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Q")
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -180,7 +186,10 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
if tool.Ifc.get_entity(bpy.context.active_object):
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
else:
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
|
||||
@@ -503,6 +503,7 @@ class DisableEditingAssignedMaterial(bpy.types.Operator):
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.bim.disable_editing_material_set_item()
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
props = obj.BIMObjectMaterialProperties
|
||||
props.is_editing = False
|
||||
@@ -526,6 +527,9 @@ class EditAssignedMaterial(bpy.types.Operator):
|
||||
props = obj.BIMObjectMaterialProperties
|
||||
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
|
||||
|
||||
if props.active_material_set_item_id != 0: # We were editing a material layer set item
|
||||
bpy.ops.bim.edit_material_set_item(material_set_item=props.active_material_set_item_id)
|
||||
|
||||
if product_data["type"] == "IfcMaterial":
|
||||
bpy.ops.bim.unassign_material(obj=obj.name)
|
||||
bpy.ops.bim.assign_material(obj=obj.name, material_type="IfcMaterial")
|
||||
|
||||
@@ -188,14 +188,17 @@ class AuthoringData:
|
||||
if not ifc_class and ifc_classes:
|
||||
ifc_class = ifc_classes[0][0]
|
||||
if ifc_class:
|
||||
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name)
|
||||
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name or "Unnamed")
|
||||
results.extend(elements)
|
||||
return results
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def relating_types(cls, ifc_class=None):
|
||||
return [(str(e.id()), e.Name, e.Description or "") for e in cls.constr_class_entities(ifc_class=ifc_class)]
|
||||
return [
|
||||
(str(e.id()), e.Name or "Unnamed", e.Description or "")
|
||||
for e in cls.constr_class_entities(ifc_class=ifc_class)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def relating_types_browser(cls):
|
||||
@@ -331,7 +334,7 @@ class ArrayData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = { "parameters": cls.parameters() }
|
||||
cls.data = {"parameters": cls.parameters()}
|
||||
|
||||
@classmethod
|
||||
def parameters(cls):
|
||||
|
||||
@@ -201,7 +201,9 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
page: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
context.scene.BIMModelProperties.type_page = self.page
|
||||
props = context.scene.BIMModelProperties
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=props.type_class, offset=9 * (self.page - 1), limit=9)
|
||||
props.type_page = self.page
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -316,14 +318,24 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Load Type Thumbnails"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
limit: bpy.props.IntProperty()
|
||||
offset: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
processing = set()
|
||||
# Only process at most one class at a time.
|
||||
# Only process at most one paginated class at a time.
|
||||
# Large projects have hundreds of types which can lead to unnecessary lag.
|
||||
queue = tool.Ifc.get().by_type(self.ifc_class)
|
||||
queue = sorted(tool.Ifc.get().by_type(self.ifc_class), key=lambda e: e.Name or "Unnamed")
|
||||
if self.limit:
|
||||
queue = queue[self.offset:self.offset + self.limit]
|
||||
else:
|
||||
offset = 9 * (props.type_page - 1)
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
queue = queue[offset:offset + 9]
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
|
||||
@@ -173,10 +173,9 @@ class DumbSlabGenerator:
|
||||
matrix_world[2][3] = self.collection_obj.location[2] - self.depth
|
||||
else:
|
||||
matrix_world[2][3] -= self.depth
|
||||
obj.matrix_world = matrix_world
|
||||
obj.matrix_world = Matrix.Rotation(self.x_angle, 4, 'X') @ matrix_world
|
||||
bpy.context.view_layer.update()
|
||||
self.collection.objects.link(obj)
|
||||
obj.matrix_world = Matrix.Rotation(self.x_angle, 4, 'X')
|
||||
|
||||
element = blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
@@ -618,122 +617,13 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
self.vertices = []
|
||||
self.edges = []
|
||||
self.arcs = []
|
||||
self.circles = []
|
||||
|
||||
profile = extrusion.SweptArea
|
||||
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
||||
self.process_curve(obj, position, profile.OuterCurve)
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
for inner_curve in profile.InnerCurves:
|
||||
self.process_curve(obj, position, inner_curve)
|
||||
elif profile.is_a() == "IfcRectangleProfileDef":
|
||||
self.process_rectangle(obj, position, profile)
|
||||
|
||||
mesh = bpy.data.meshes.new("Profile")
|
||||
mesh.from_pydata(self.vertices, self.edges, [])
|
||||
mesh.BIMMeshProperties.is_profile = True
|
||||
obj.data = mesh
|
||||
|
||||
for arc in self.arcs:
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
group.add(arc, 1, "REPLACE")
|
||||
|
||||
for circle in self.circles:
|
||||
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
||||
group.add(circle, 1, "REPLACE")
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
DecorationsHandler.install(context)
|
||||
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
def process_curve(self, obj, position, curve):
|
||||
offset = len(self.vertices)
|
||||
|
||||
if curve.is_a("IfcPolyline"):
|
||||
total_points = len(curve.Points)
|
||||
last_index = len(curve.Points) - 1
|
||||
for i, point in enumerate(curve.Points):
|
||||
if i == last_index:
|
||||
continue
|
||||
global_point = position @ Vector(self.convert_unit_to_si(point.Coordinates)).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
|
||||
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
|
||||
elif curve.is_a("IfcIndexedPolyCurve"):
|
||||
is_arc = False
|
||||
if curve.Segments:
|
||||
for segment in curve.Segments:
|
||||
if len(segment[0]) == 3: # IfcArcIndex
|
||||
is_arc = True
|
||||
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
|
||||
global_point = position @ Vector(local_point).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][1] - 1])
|
||||
global_point = position @ Vector(local_point).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
self.arcs.append([len(self.vertices) - 2, len(self.vertices) - 1])
|
||||
else:
|
||||
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
|
||||
global_point = position @ Vector(local_point).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
if is_arc:
|
||||
self.arcs[-1].append(len(self.vertices) - 1)
|
||||
is_arc = False
|
||||
else:
|
||||
for local_point in curve.Points.CoordList:
|
||||
global_point = position @ Vector(self.convert_unit_to_si(local_point)).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
# Curves without segments are self closing
|
||||
del self.vertices[-1]
|
||||
|
||||
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
|
||||
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
|
||||
elif curve.is_a("IfcCircle"):
|
||||
center = self.convert_unit_to_si(
|
||||
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()
|
||||
)
|
||||
radius = self.convert_unit_to_si(curve.Radius)
|
||||
self.vertices.extend(
|
||||
[
|
||||
position @ Vector((center[0], center[1] - curve.Radius, 0.0)),
|
||||
position @ Vector((center[0], center[1] + curve.Radius, 0.0)),
|
||||
]
|
||||
)
|
||||
self.circles.append([offset, offset + 1])
|
||||
self.edges.append((offset, offset + 1))
|
||||
|
||||
def process_rectangle(self, obj, position, profile):
|
||||
if profile.Position:
|
||||
p_position = Matrix(ifcopenshell.util.placement.get_axis2placement(profile.Position).tolist())
|
||||
p_position[0][3] *= self.unit_scale
|
||||
p_position[1][3] *= self.unit_scale
|
||||
p_position[2][3] *= self.unit_scale
|
||||
else:
|
||||
p_position = Matrix()
|
||||
|
||||
x = self.convert_unit_to_si(profile.XDim)
|
||||
y = self.convert_unit_to_si(profile.YDim)
|
||||
|
||||
self.vertices.extend(
|
||||
[
|
||||
position @ p_position @ Vector((-x / 2, -y / 2, 0.0)),
|
||||
position @ p_position @ Vector((x / 2, -y / 2, 0.0)),
|
||||
position @ p_position @ Vector((x / 2, y / 2, 0.0)),
|
||||
position @ p_position @ Vector((-x / 2, y / 2, 0.0)),
|
||||
]
|
||||
)
|
||||
self.edges.extend([(i, i + 1) for i in range(0, len(self.vertices))])
|
||||
self.edges[-1] = (len(self.vertices) - 1, 0) # Close the loop
|
||||
|
||||
def convert_unit_to_si(self, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v * self.unit_scale for v in value]
|
||||
return value * self.unit_scale
|
||||
|
||||
|
||||
class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_extrusion_profile"
|
||||
@@ -758,40 +648,16 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
helper = Helper(tool.Ifc.get())
|
||||
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
|
||||
|
||||
if isinstance(indices, tuple) and indices[0] is False: # Ugly
|
||||
profile = tool.Model.export_profile(obj, position=position)
|
||||
|
||||
if not profile:
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE: " + indices[1])
|
||||
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
DecorationsHandler.install(context)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
return {"FINISHED"}
|
||||
|
||||
self.bm = bmesh.new()
|
||||
self.bm.from_mesh(obj.data)
|
||||
self.bm.verts.ensure_lookup_table()
|
||||
self.bm.edges.ensure_lookup_table()
|
||||
|
||||
if indices["inner_curves"]:
|
||||
profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA")
|
||||
else:
|
||||
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
self.points = self.create_points(position, indices["points"])
|
||||
|
||||
profile.OuterCurve = self.create_curve(position, indices["profile"])
|
||||
if indices["inner_curves"]:
|
||||
results = []
|
||||
for inner_curve in indices["inner_curves"]:
|
||||
results.append(self.create_curve(position, inner_curve))
|
||||
profile.InnerCurves = results
|
||||
|
||||
self.bm.free()
|
||||
return
|
||||
|
||||
old_profile = extrusion.SweptArea
|
||||
extrusion.SweptArea = profile
|
||||
@@ -831,45 +697,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def create_points(self, position, indices):
|
||||
position_i = position.inverted()
|
||||
points = []
|
||||
for point in indices:
|
||||
local_point = (position_i @ point).to_2d()
|
||||
points.append(self.convert_si_to_unit(list(local_point)))
|
||||
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
|
||||
def create_curve(self, position, edge_indices, points=None):
|
||||
position_i = position.inverted()
|
||||
if len(edge_indices) == 2:
|
||||
diameter = edge_indices[0]
|
||||
p1 = self.bm.verts[diameter[0]].co
|
||||
p2 = self.bm.verts[diameter[1]].co
|
||||
center = self.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
|
||||
radius = self.convert_si_to_unit((p1 - p2).length / 2)
|
||||
return tool.Ifc.get().createIfcCircle(
|
||||
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius
|
||||
)
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
points = []
|
||||
for edge in edge_indices:
|
||||
local_point = (position_i @ Vector(self.bm.verts[edge[0]].co)).to_2d()
|
||||
points.append(tool.Ifc.get().createIfcCartesianPoint(self.convert_si_to_unit(local_point)))
|
||||
points.append(points[0])
|
||||
return tool.Ifc.get().createIfcPolyline(points)
|
||||
segments = []
|
||||
for segment in edge_indices:
|
||||
if len(segment) == 2:
|
||||
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
|
||||
elif len(segment) == 3:
|
||||
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
|
||||
return tool.Ifc.get().createIfcIndexedPolyCurve(self.points, segments, False)
|
||||
|
||||
def convert_si_to_unit(self, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v / self.unit_scale for v in value]
|
||||
return value / self.unit_scale
|
||||
|
||||
|
||||
class ResetVertex(bpy.types.Operator):
|
||||
bl_idname = "bim.reset_vertex"
|
||||
|
||||
@@ -38,7 +38,7 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
props.type_page = 1
|
||||
if props.ifc_class:
|
||||
props.type_class = props.ifc_class
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class)
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class, offset=0, limit=9)
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
return context.window_manager.invoke_popup(self, width=550)
|
||||
|
||||
@@ -413,7 +413,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.align_product(align_type="CENTERLINE")
|
||||
|
||||
def hotkey_S_E(self):
|
||||
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
elif self.active_class in ("IfcWall", "IfcWallStandardCase"):
|
||||
bpy.ops.bim.join_wall(join_type="T")
|
||||
elif self.active_class in ("IfcSlab", "IfcSlabStandardCase", "IfcRamp", "IfcRoof"):
|
||||
if not bpy.context.active_object:
|
||||
|
||||
@@ -20,12 +20,16 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.LoadProfiles,
|
||||
operator.DisableProfileEditingUI,
|
||||
operator.RemoveProfileDef,
|
||||
operator.EnableEditingProfile,
|
||||
operator.AddProfileDef,
|
||||
operator.DisableEditingArbitraryProfile,
|
||||
operator.DisableEditingProfile,
|
||||
operator.DisableProfileEditingUI,
|
||||
operator.EditArbitraryProfile,
|
||||
operator.EditProfile,
|
||||
operator.EnableEditingArbitraryProfile,
|
||||
operator.EnableEditingProfile,
|
||||
operator.LoadProfiles,
|
||||
operator.RemoveProfileDef,
|
||||
prop.Profile,
|
||||
prop.BIMProfileProperties,
|
||||
ui.BIM_PT_profiles,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# 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
|
||||
import ifcopenshell.util.doc
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
ProfileData.is_loaded = False
|
||||
|
||||
|
||||
class ProfileData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"total_profiles": cls.total_profiles(),
|
||||
"profile_classes": cls.profile_classes(),
|
||||
"is_arbitrary_profile": cls.is_arbitrary_profile(),
|
||||
"is_editing_arbitrary_profile": cls.is_editing_arbitrary_profile(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def total_profiles(cls):
|
||||
return len(tool.Ifc.get().by_type("IfcProfileDef"))
|
||||
|
||||
@classmethod
|
||||
def profile_classes(cls):
|
||||
classes = ["IfcArbitraryClosedProfileDef"]
|
||||
queue = ["IfcParameterizedProfileDef"]
|
||||
while queue:
|
||||
item = queue.pop()
|
||||
subtypes = [s.name() for s in tool.Ifc.schema().declaration_by_name(item).subtypes()]
|
||||
classes.extend(subtypes)
|
||||
queue.extend(subtypes)
|
||||
schema_identifier = tool.Ifc.get_schema()
|
||||
return [
|
||||
(c, c, ifcopenshell.util.doc.get_entity_doc(schema_identifier, c)["description"] or "")
|
||||
for c in sorted(classes)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_arbitrary_profile(cls):
|
||||
props = bpy.context.scene.BIMProfileProperties
|
||||
if props.active_profile_id:
|
||||
profile = tool.Ifc.get().by_id(props.active_profile_id)
|
||||
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_editing_arbitrary_profile(cls):
|
||||
obj = bpy.context.active_object
|
||||
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -19,8 +19,8 @@
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import blenderbim.bim.helper
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.profile.data import Data
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.module.model.slab import DecorationsHandler
|
||||
|
||||
|
||||
class LoadProfiles(bpy.types.Operator):
|
||||
@@ -32,10 +32,11 @@ class LoadProfiles(bpy.types.Operator):
|
||||
props = context.scene.BIMProfileProperties
|
||||
props.profiles.clear()
|
||||
|
||||
for ifc_definition_id, profile in Data.profiles.items():
|
||||
for profile in tool.Ifc.get().by_type("IfcProfileDef"):
|
||||
new = props.profiles.add()
|
||||
new.ifc_definition_id = ifc_definition_id
|
||||
new.name = profile.get("ProfileName", "") or "Unnamed"
|
||||
new.ifc_definition_id = profile.id()
|
||||
new.name = profile.ProfileName or "Unnamed"
|
||||
new.ifc_class = profile.is_a()
|
||||
|
||||
props.is_editing = True
|
||||
bpy.ops.bim.disable_editing_profile()
|
||||
@@ -52,39 +53,28 @@ class DisableProfileEditingUI(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveProfileDef(bpy.types.Operator):
|
||||
class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_profile_def"
|
||||
bl_label = "Remove Profile Definition"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
profile: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run("profile.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)})
|
||||
Data.load(self.file)
|
||||
ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
|
||||
bpy.ops.bim.load_profiles()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingProfile(bpy.types.Operator):
|
||||
class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_profile"
|
||||
bl_label = "Enable Editing Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
profile: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
props.profile_attributes.clear()
|
||||
|
||||
data = Data.profiles[self.profile]
|
||||
|
||||
blenderbim.bim.helper.import_attributes(data["type"], props.profile_attributes, data)
|
||||
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes)
|
||||
props.active_profile_id = self.profile
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingProfile(bpy.types.Operator):
|
||||
@@ -97,20 +87,91 @@ class DisableEditingProfile(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditProfile(bpy.types.Operator):
|
||||
class EditProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_profile"
|
||||
bl_label = "Edit Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
attributes = blenderbim.bim.helper.export_attributes(props.profile_attributes)
|
||||
self.file = IfcStore.get_file()
|
||||
profile = self.file.by_id(props.active_profile_id)
|
||||
ifcopenshell.api.run("profile.edit_profile", self.file, **{"profile": profile, "attributes": attributes})
|
||||
Data.load(IfcStore.get_file())
|
||||
profile = tool.Ifc.get().by_id(props.active_profile_id)
|
||||
ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes)
|
||||
bpy.ops.bim.load_profiles()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_profile_def"
|
||||
bl_label = "Add Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
profile_class = props.profile_classes
|
||||
if profile_class == "IfcArbitraryClosedProfileDef":
|
||||
points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]
|
||||
profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points)
|
||||
else:
|
||||
profile = ifcopenshell.api.run("profile.add_parameterized_profile", tool.Ifc.get(), ifc_class=profile_class)
|
||||
profile.ProfileName = "New Profile"
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
|
||||
class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_arbitrary_profile"
|
||||
bl_label = "Enable Editing Arbitrary Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
profile = tool.Ifc.get().by_id(props.active_profile_id)
|
||||
obj = tool.Model.import_profile(profile)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
DecorationsHandler.install(context)
|
||||
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
|
||||
|
||||
|
||||
class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_arbitrary_profile"
|
||||
bl_label = "Disable Editing Arbitrary Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
DecorationsHandler.uninstall()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
|
||||
class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_arbitrary_profile"
|
||||
bl_label = "Edit Arbitrary Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
old_profile = tool.Ifc.get().by_id(props.active_profile_id)
|
||||
|
||||
obj = context.active_object
|
||||
|
||||
DecorationsHandler.uninstall()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
profile = tool.Model.export_profile(obj)
|
||||
if not profile:
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE: " + indices[1])
|
||||
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
DecorationsHandler.install(context)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
return
|
||||
|
||||
bpy.data.objects.remove(obj)
|
||||
profile.ProfileType = old_profile.ProfileType
|
||||
profile.ProfileName = old_profile.ProfileName
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
|
||||
bpy.ops.bim.load_profiles()
|
||||
props.active_profile_id = profile.id()
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.attribute
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.module.profile.data import ProfileData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -35,8 +36,15 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def get_profile_classes(self, context):
|
||||
if not ProfileData.is_loaded:
|
||||
ProfileData.load()
|
||||
return ProfileData.data["profile_classes"]
|
||||
|
||||
|
||||
class Profile(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_class: StringProperty(name="IFC Class")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
|
||||
@@ -46,3 +54,4 @@ class BIMProfileProperties(PropertyGroup):
|
||||
active_profile_index: IntProperty(name="Active Profile Index")
|
||||
active_profile_id: IntProperty(name="Active Profile Id")
|
||||
profile_attributes: CollectionProperty(name="Profile Attributes", type=Attribute)
|
||||
profile_classes: EnumProperty(items=get_profile_classes, name="Profile Classes")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -17,9 +17,9 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.profile.data import Data
|
||||
from blenderbim.bim.module.profile.data import ProfileData
|
||||
|
||||
|
||||
class BIM_PT_profiles(Panel):
|
||||
@@ -33,17 +33,15 @@ class BIM_PT_profiles(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = IfcStore.get_file()
|
||||
return file
|
||||
return tool.Ifc.get()
|
||||
|
||||
def draw(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
if not Data.is_loaded:
|
||||
Data.load(self.file)
|
||||
if not ProfileData.is_loaded:
|
||||
ProfileData.load()
|
||||
self.props = context.scene.BIMProfileProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Profiles Found".format(len(Data.profiles)), icon="SNAP_GRID")
|
||||
row.label(text="{} Profiles Found".format(ProfileData.data["total_profiles"]), icon="SNAP_GRID")
|
||||
if self.props.is_editing:
|
||||
row.operator("bim.disable_profile_editing_ui", text="", icon="CANCEL")
|
||||
else:
|
||||
@@ -52,6 +50,10 @@ class BIM_PT_profiles(Panel):
|
||||
if not self.props.is_editing:
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "profile_classes", text="")
|
||||
row.operator("bim.add_profile_def", text="", icon="ADD")
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_profiles",
|
||||
"",
|
||||
@@ -65,6 +67,14 @@ class BIM_PT_profiles(Panel):
|
||||
self.draw_editable_ui(context)
|
||||
|
||||
def draw_editable_ui(self, context):
|
||||
if ProfileData.data["is_arbitrary_profile"]:
|
||||
if ProfileData.data["is_editing_arbitrary_profile"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_arbitrary_profile", text="Save Profile", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_arbitrary_profile", text="Edit Profile", icon="GREASEPENCIL")
|
||||
blenderbim.bim.helper.draw_attributes(self.props.profile_attributes, self.layout)
|
||||
|
||||
|
||||
@@ -74,6 +84,7 @@ class BIM_UL_profiles(UIList):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name or "Unnamed")
|
||||
row.label(text=item.ifc_class)
|
||||
|
||||
if props.active_profile_id == item.ifc_definition_id:
|
||||
row.operator("bim.edit_profile", text="", icon="CHECKMARK")
|
||||
|
||||
@@ -1,10 +1,578 @@
|
||||
mapper = {
|
||||
"Qto_WallBaseQuantities" : {
|
||||
"Length" : "get_length",
|
||||
"Width" : "get_width",
|
||||
"Height" : "get_height",
|
||||
'Qto_AudioVisualApplianceBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
"Qto_SlabBaseQuantities" : {
|
||||
"Depth" : "get_height",
|
||||
'Qto_PlateBaseQuantities' : {
|
||||
'Width' : None,
|
||||
'Perimeter' : None,
|
||||
'GrossArea' : None,
|
||||
'NetArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_OpeningElementBaseQuantities' : {
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
'Depth' : None,
|
||||
'Area' : None,
|
||||
'Volume' : None,
|
||||
},
|
||||
'Qto_MarineFacilityBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
'Area' : None,
|
||||
'Volume' : None,
|
||||
},
|
||||
'Qto_ChillerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_PileBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'CrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossSurfaceArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_VibrationIsolatorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_LampBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_VehicleBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
},
|
||||
'Qto_PipeFittingBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'GrossCrossSectionArea' : None,
|
||||
'NetCrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_CableCarrierFittingBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_HeatExchangerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_DoorBaseQuantities' : {
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
'Perimeter' : None,
|
||||
'Area' : None,
|
||||
},
|
||||
'Qto_DuctSegmentBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'GrossCrossSectionArea' : None,
|
||||
'NetCrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_TransformerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_FacilityPartBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
'Area' : None,
|
||||
'Volume' : None,
|
||||
},
|
||||
'Qto_ProjectionElementBaseQuantities' : {
|
||||
'Area' : None,
|
||||
'Volume' : None,
|
||||
},
|
||||
'Qto_SignBaseQuantities' : {
|
||||
'Height' : None,
|
||||
'Width' : None,
|
||||
'Thickness' : None,
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_CableSegmentBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'Length' : None,
|
||||
'CrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
},
|
||||
'Qto_BuildingBaseQuantities' : {
|
||||
'Height' : None,
|
||||
'EavesHeight' : None,
|
||||
'FootPrintArea' : None,
|
||||
'GrossFloorArea' : None,
|
||||
'NetFloorArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
},
|
||||
'Qto_ElectricFlowStorageDeviceBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_CommunicationsApplianceBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_RailBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Volume' : None,
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_PictorialSignQuantities' : {
|
||||
'Area' : None,
|
||||
'SignArea' : None,
|
||||
},
|
||||
'Qto_SpaceHeaterBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_CoveringBaseQuantities' : {
|
||||
'Width' : None,
|
||||
'GrossArea' : None,
|
||||
'NetArea' : None,
|
||||
},
|
||||
'Qto_PipeSegmentBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'GrossCrossSectionArea' : None,
|
||||
'NetCrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
'FootPrintArea' : None,
|
||||
},
|
||||
'Qto_HumidifierBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_ConstructionEquipmentResourceBaseQuantities' : {
|
||||
'UsageTime' : None,
|
||||
'OperatingTime' : None,
|
||||
},
|
||||
'Qto_AlarmBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_JunctionBoxBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'NumberOfGangs' : None,
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
},
|
||||
'Qto_ArealStratumBaseQuantities' : {
|
||||
'Area' : None,
|
||||
'Length' : None,
|
||||
'PlanLength' : None,
|
||||
},
|
||||
'Qto_SiteBaseQuantities' : {
|
||||
'GrossPerimeter' : None,
|
||||
'GrossArea' : None,
|
||||
},
|
||||
'Qto_MotorConnectionBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_RoofBaseQuantities' : {
|
||||
'GrossArea' : None,
|
||||
'NetArea' : None,
|
||||
'ProjectedArea' : None,
|
||||
},
|
||||
'Qto_ChimneyBaseQuantities' : {
|
||||
'Length' : None,
|
||||
},
|
||||
'Qto_ElectricTimeControlBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_ElectricMotorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_EarthworksFillBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Depth' : None,
|
||||
'CompactedVolume' : None,
|
||||
'LooseVolume' : None,
|
||||
},
|
||||
'Qto_ConduitSegmentBaseQuantities' : {
|
||||
'InnerDiameter' : None,
|
||||
'OuterDiameter' : None,
|
||||
},
|
||||
'Qto_SignalBaseQuantities' : {
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_DuctFittingBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'GrossCrossSectionArea' : None,
|
||||
'NetCrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_UnitaryControlElementBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_ActuatorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_CurtainWallQuantities' : {
|
||||
'Length' : None,
|
||||
'Height' : None,
|
||||
'Width' : None,
|
||||
'GrossSideArea' : None,
|
||||
'NetSideArea' : None,
|
||||
},
|
||||
'Qto_BoilerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
'TotalSurfaceArea' : None,
|
||||
},
|
||||
'Qto_FlowMeterBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_AirTerminalBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'Perimeter' : None,
|
||||
'TotalSurfaceArea' : None,
|
||||
},
|
||||
'Qto_DuctSilencerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_WasteTerminalBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_SlabBaseQuantities' : {
|
||||
'Width' : None,
|
||||
'Length' : None,
|
||||
'Depth' : "get_height",
|
||||
'Perimeter' : None,
|
||||
'GrossArea' : None,
|
||||
'NetArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_ImpactProtectionDeviceBaseQuantities' : {
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_LightFixtureBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_FlowInstrumentBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_BuildingStoreyBaseQuantities' : {
|
||||
'GrossHeight' : None,
|
||||
'NetHeight' : None,
|
||||
'GrossPerimeter' : None,
|
||||
'GrossFloorArea' : None,
|
||||
'NetFloorArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
},
|
||||
'Qto_ReinforcedSoilBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Depth' : None,
|
||||
'Area' : None,
|
||||
'Volume' : None,
|
||||
},
|
||||
'Qto_DistributionBoardBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'NumberOfCircuits' : None,
|
||||
},
|
||||
'Qto_FootingBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
'CrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossSurfaceArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_PumpBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_CableCarrierSegmentBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'Length' : None,
|
||||
'CrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
},
|
||||
'Qto_InterceptorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_ColumnBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'CrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossSurfaceArea' : None,
|
||||
'NetSurfaceArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_EarthworksCutBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Depth' : None,
|
||||
'UndisturbedVolume' : None,
|
||||
'LooseVolume' : None,
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_StackTerminalBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_CoilBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_KerbBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
'Depth' : None,
|
||||
'Volume' : None,
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_PavementBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Depth' : None,
|
||||
'GrossArea' : None,
|
||||
'NetArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
},
|
||||
'Qto_ControllerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_SolarDeviceBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'GrossArea' : None,
|
||||
},
|
||||
'Qto_RampFlightBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'GrossArea' : None,
|
||||
'NetArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
},
|
||||
'Qto_ElectricApplianceBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_ValveBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_DamperBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_SurfaceFeatureBaseQuantities' : {
|
||||
'Area' : None,
|
||||
'Length' : None,
|
||||
},
|
||||
'Qto_WallBaseQuantities' : {
|
||||
'Length' : "get_length",
|
||||
'Width' : "get_width",
|
||||
'Height' : None,
|
||||
'GrossFootPrintArea' : None,
|
||||
'NetFootPrintArea' : None,
|
||||
'GrossSideArea' : None,
|
||||
'NetSideArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_StairFlightBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
},
|
||||
'Qto_SwitchingDeviceBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_BurnerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_ConstructionMaterialResourceBaseQuantities' : {
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_ElectricGeneratorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_LinearStratumBaseQuantities' : {
|
||||
'Diameter' : None,
|
||||
'Length' : None,
|
||||
},
|
||||
'Qto_CableFittingBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_DistributionChamberElementBaseQuantities' : {
|
||||
'GrossSurfaceArea' : None,
|
||||
'NetSurfaceArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'Depth' : None,
|
||||
},
|
||||
'Qto_CompressorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_ProtectiveDeviceTrippingUnitBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_EvaporatorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_SpaceBaseQuantities' : {
|
||||
'Height' : None,
|
||||
'FinishCeilingHeight' : None,
|
||||
'FinishFloorHeight' : None,
|
||||
'GrossPerimeter' : None,
|
||||
'NetPerimeter' : None,
|
||||
'GrossFloorArea' : None,
|
||||
'NetFloorArea' : None,
|
||||
'GrossWallArea' : None,
|
||||
'NetWallArea' : None,
|
||||
'GrossCeilingArea' : None,
|
||||
'NetCeilingArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
},
|
||||
'Qto_CourseBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Thickness' : None,
|
||||
'Volume' : None,
|
||||
'GrossVolume' : None,
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_CondenserBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_FireSuppressionTerminalBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_RailingBaseQuantities' : {
|
||||
'Length' : None,
|
||||
},
|
||||
'Qto_TubeBundleBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_BeamBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'CrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossSurfaceArea' : None,
|
||||
'NetSurfaceArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_SleeperBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
},
|
||||
'Qto_ProtectiveDeviceBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_CooledBeamBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_TankBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
'TotalSurfaceArea' : None,
|
||||
},
|
||||
'Qto_AirToAirHeatRecoveryBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_CoolingTowerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_SensorBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_WindowBaseQuantities' : {
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
'Perimeter' : None,
|
||||
'Area' : None,
|
||||
},
|
||||
'Qto_SanitaryTerminalBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_BuildingElementProxyQuantities' : {
|
||||
'NetSurfaceArea' : None,
|
||||
'NetVolume' : None,
|
||||
},
|
||||
'Qto_FanBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_OutletBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_UnitaryEquipmentBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_FilterBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_MemberBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'CrossSectionArea' : None,
|
||||
'OuterSurfaceArea' : None,
|
||||
'GrossSurfaceArea' : None,
|
||||
'NetSurfaceArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'GrossWeight' : None,
|
||||
'NetWeight' : None,
|
||||
},
|
||||
'Qto_BodyGeometryValidation' : {
|
||||
'GrossSurfaceArea' : None,
|
||||
'NetSurfaceArea' : None,
|
||||
'GrossVolume' : None,
|
||||
'NetVolume' : None,
|
||||
'SurfaceGenusBeforeFeatures' : None,
|
||||
'SurfaceGenusAfterFeatures' : None,
|
||||
},
|
||||
'Qto_VolumetricStratumBaseQuantities' : {
|
||||
'Area' : None,
|
||||
'Mass' : None,
|
||||
'PlanArea' : None,
|
||||
'Volume' : None,
|
||||
},
|
||||
'Qto_SpatialZoneBaseQuantities' : {
|
||||
'Length' : None,
|
||||
'Width' : None,
|
||||
'Height' : None,
|
||||
},
|
||||
'Qto_ReinforcingElementBaseQuantities' : {
|
||||
'Count' : None,
|
||||
'Length' : None,
|
||||
'Weight' : None,
|
||||
},
|
||||
'Qto_EvaporativeCoolerBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_AirTerminalBoxTypeBaseQuantities' : {
|
||||
'GrossWeight' : None,
|
||||
},
|
||||
'Qto_LaborResourceBaseQuantities' : {
|
||||
'StandardWork' : None,
|
||||
'OvertimeWork' : None,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ class QtoCalculator:
|
||||
|
||||
for key in self.mapping_dict.keys():
|
||||
for item in self.mapping_dict[key].keys():
|
||||
self.mapping_dict[key][item] = eval("self." + self.mapping_dict[key][item])
|
||||
if self.mapping_dict[key][item]:
|
||||
self.mapping_dict[key][item] = eval("self." + self.mapping_dict[key][item])
|
||||
else:
|
||||
self.mapping_dict[key][item] = None
|
||||
|
||||
def calculate_quantity(self, qto_name, quantity_name, obj):
|
||||
return self.mapping_dict[qto_name][quantity_name](obj)
|
||||
|
||||
@@ -279,7 +279,7 @@ class Attribute(PropertyGroup):
|
||||
has_calculator: BoolProperty(name="Has Calculator", default=False)
|
||||
|
||||
def get_value(self):
|
||||
if self.is_null:
|
||||
if self.is_optional and self.is_null:
|
||||
return None
|
||||
return getattr(self, str(self.get_value_name()), None)
|
||||
|
||||
|
||||
@@ -388,6 +388,12 @@ def draw_custom_context_menu(self, context):
|
||||
if not hasattr(context, "button_pointer") or not hasattr(context, "button_prop"):
|
||||
return
|
||||
# https://blender.stackexchange.com/a/275555/86891
|
||||
if (
|
||||
not hasattr(context, "button_pointer")
|
||||
or not hasattr(context, "button_prop")
|
||||
or not hasattr(context.button_prop, "identifier")
|
||||
):
|
||||
return
|
||||
prop = context.button_pointer
|
||||
prop_name = context.button_prop.identifier
|
||||
prop_value = getattr(prop, prop_name, None)
|
||||
|
||||
@@ -92,14 +92,19 @@ class Model(blenderbim.core.tool.Model):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = 0.0
|
||||
thickness = 0.0
|
||||
direction_sense = "POSITIVE"
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material:
|
||||
if material.is_a("IfcMaterialLayerSetUsage"):
|
||||
offset = material.OffsetFromReferenceLine * unit_scale
|
||||
direction_sense = material.DirectionSense
|
||||
material = material.ForLayerSet
|
||||
if material.is_a("IfcMaterialLayerSet"):
|
||||
thickness = sum([l.LayerThickness for l in material.MaterialLayers]) * unit_scale
|
||||
return {"thickness": thickness, "offset": offset}
|
||||
if direction_sense == "NEGATIVE":
|
||||
thickness *= -1
|
||||
offset *= -1
|
||||
return {"thickness": thickness, "offset": offset, "direction_sense": direction_sense}
|
||||
|
||||
@classmethod
|
||||
def get_manual_booleans(cls, element):
|
||||
|
||||
@@ -42,7 +42,7 @@ class Project(blenderbim.core.tool.Project):
|
||||
def load_default_thumbnails(cls):
|
||||
if tool.Ifc.get().by_type("IfcElementType"):
|
||||
ifc_class = sorted(tool.Ifc.get().by_type("IfcElementType"), key=lambda e: e.is_a())[0].is_a()
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
|
||||
|
||||
@classmethod
|
||||
def run_aggregate_assign_object(cls, relating_obj=None, related_obj=None):
|
||||
|
||||
@@ -42,8 +42,8 @@ IfcOpenShell (using an IfcOpenBot build) for convenience. Instructions on how to
|
||||
compile IfcOpenShell is out of scope of this document.
|
||||
|
||||
You can create your own package by using the Makefile as shown below. You can
|
||||
choose between a ``PLATFORM`` of ``linux``, ``macos``, and ``win``. You can
|
||||
choose between a ``PYVERSION`` of ``py39``, ``py37``, or ``py310``.
|
||||
choose between a ``PLATFORM`` of ``linux``, ``macos``, ``macosm1``, and ``win``.
|
||||
You can choose between a ``PYVERSION`` of ``py39``, ``py37``, or ``py310``.
|
||||
::
|
||||
|
||||
$ cd src/blenderbim
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 @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/>.
|
||||
|
||||
# fmt: off
|
||||
# pylint: skip-file
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import boltspy as bolts
|
||||
|
||||
|
||||
class LibraryGenerator:
|
||||
def generate(self, parse_profiles_type="EU", output_filename="IFC4 EU Steel.ifc"):
|
||||
ifcopenshell.api.pre_listeners = {}
|
||||
ifcopenshell.api.post_listeners = {}
|
||||
|
||||
self.materials = {}
|
||||
|
||||
self.file = ifcopenshell.api.run("project.create_file")
|
||||
self.project = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class="IfcProject", name=f"{parse_profiles_type} Steel Profiles Library"
|
||||
)
|
||||
self.library = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=f"{parse_profiles_type} Steel Profiles Library"
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
|
||||
)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
|
||||
|
||||
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
|
||||
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
|
||||
self.representations = {
|
||||
"body": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="MODEL_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"annotation": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Plan",
|
||||
context_identifier="Annotation",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=plan,
|
||||
),
|
||||
}
|
||||
|
||||
self.material = ifcopenshell.api.run("material.add_material", self.file, name="Unknown")
|
||||
|
||||
profiles_translation = {
|
||||
"profile_i": ("IfcIShapeProfileDef", {"tw": "WebThickness", "tf": "FlangeThickness", "h": "OverallDepth", "b": "OverallWidth"}),
|
||||
"profile_z": ("IfcZShapeProfileDef", {"tw": "WebThickness", "tf": "FlangeThickness", "h": "Depth", "c1": "FlangeWidth"}),
|
||||
"profile_c": ("IfcUShapeProfileDef", {"tw": "WebThickness", "tf": "FlangeThickness", "h": "Depth", "b": "FlangeWidth"}),
|
||||
"profile_l*_equal": ("IfcLShapeProfileDef", {"a": "Depth", "t": "Thickness"}),
|
||||
"profile_l*_unequal": ("IfcLShapeProfileDef", {"a": "Depth", "b": "Width", "t": "Thickness"}),
|
||||
"profile_hollow*_circle": ("IfcCircleHollowProfileDef", {"t": "WallThickness", "D": "Radius"}),
|
||||
"profile_hollow*_square": ("IfcRectangleHollowProfileDef", {"t": "WallThickness", "b": "XDim"}),
|
||||
"profile_hollow*_rectangular": ("IfcRectangleHollowProfileDef", {"t": "WallThickness", "b": "XDim", "h": "YDim"}),
|
||||
}
|
||||
|
||||
if parse_profiles_type == "AU":
|
||||
bolt_class_filter = lambda x: "bluescope" in x.id
|
||||
elif parse_profiles_type == "EU":
|
||||
bolt_class_filter = lambda x: "bluescope" not in x.id
|
||||
else:
|
||||
bolt_class_filter = lambda x: True
|
||||
|
||||
for prof_type in profiles_translation:
|
||||
ifc_profile_name, ifc_params_translation = profiles_translation[prof_type]
|
||||
col_name, _, prof_keyword = prof_type.partition("*")
|
||||
bolt_col = bolts.repo.collections[col_name]
|
||||
|
||||
for bolt_class in bolts.repo.collection_classes.get_dsts(bolt_col):
|
||||
if not bolt_class_filter(bolt_class):
|
||||
continue
|
||||
|
||||
if prof_keyword and prof_keyword not in bolt_class.id:
|
||||
continue
|
||||
|
||||
if not bolt_class.parameters.tables:
|
||||
# some bolt classes have no data attached
|
||||
# like hollow_generic_square
|
||||
continue
|
||||
|
||||
bolts_cols = bolt_class.parameters.tables[0].columns
|
||||
bolts_cols = [ifc_params_translation.get(c, "unused") for c in bolts_cols]
|
||||
bolts_data = bolt_class.parameters.tables[0].data
|
||||
|
||||
for prof_name in bolts_data.keys():
|
||||
ifc_params = dict(zip(bolts_cols, bolts_data[prof_name]))
|
||||
if "unused" in ifc_params:
|
||||
del ifc_params["unused"]
|
||||
|
||||
if prof_type == "profile_hollow*_square":
|
||||
ifc_params["YDim"] = ifc_params["XDim"]
|
||||
elif prof_type == "profile_hollow*_circle":
|
||||
# by default bolts provides diameter, so we need to convert it to radius
|
||||
ifc_params["Radius"] /= 2
|
||||
|
||||
# profile is setup by type of profile and by supplying it's parameters
|
||||
# ProfileType stays AREA
|
||||
profile = self.file.create_entity(ifc_profile_name, ProfileType="AREA", **ifc_params)
|
||||
|
||||
# building profiles for each of 3 types
|
||||
self.create_profile_type("IfcBeamType", prof_name, profile)
|
||||
self.create_profile_type("IfcMemberType", prof_name, profile)
|
||||
self.create_profile_type("IfcColumnType", prof_name, profile)
|
||||
|
||||
self.file.write(output_filename)
|
||||
|
||||
def create_layer_set_type(self, name, data):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=data["type"], name=name)
|
||||
element.Description = data["Description"]
|
||||
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
|
||||
layer_set = rel.RelatingMaterial
|
||||
for layer_data in data["Layers"]:
|
||||
layer = ifcopenshell.api.run(
|
||||
"material.add_layer", self.file, layer_set=layer_set, material=self.materials[layer_data[1]]["ifc"]
|
||||
)
|
||||
layer.Name = layer_data[0]
|
||||
layer.LayerThickness = layer_data[2]
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
return element
|
||||
|
||||
def create_layer_type(self, ifc_class, name, thickness):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
|
||||
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
|
||||
layer_set = rel.RelatingMaterial
|
||||
layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=layer_set, material=self.materials["TBD"]["ifc"])
|
||||
layer.LayerThickness = thickness
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
return element
|
||||
|
||||
def create_profile_type(self, ifc_class, name, profile):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
|
||||
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet")
|
||||
profile_set = rel.RelatingMaterial
|
||||
material_profile = ifcopenshell.api.run(
|
||||
"material.add_profile", self.file, profile_set=profile_set, material=self.material
|
||||
# material=self.materials["TBD"]["ifc"]
|
||||
)
|
||||
ifcopenshell.api.run("material.assign_profile", self.file, material_profile=material_profile, profile=profile)
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
|
||||
def create_type(self, ifc_class, name, representations):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
|
||||
for rep_name, obj_name in representations.items():
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_representation",
|
||||
self.file,
|
||||
context=self.representations[rep_name],
|
||||
blender_object=obj,
|
||||
geometry=obj.data,
|
||||
total_items=max(1, len(obj.material_slots)),
|
||||
)
|
||||
styles = []
|
||||
for slot in obj.material_slots:
|
||||
style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
|
||||
ifcopenshell.api.run(
|
||||
"style.add_surface_style",
|
||||
self.file,
|
||||
style=style,
|
||||
ifc_class="IfcSurfaceStyleRendering",
|
||||
attributes=tool.Style.get_surface_rendering_attributes(slot.material),
|
||||
)
|
||||
styles.append(style)
|
||||
if styles:
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", self.file, product=element, representation=representation
|
||||
)
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
LibraryGenerator().generate(parse_profiles_type="EU", output_filename="IFC4 EU Steel.ifc")
|
||||
LibraryGenerator().generate(parse_profiles_type="AU", output_filename="IFC4 AU Steel.ifc")
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
import copy
|
||||
import ifcopenshell
|
||||
|
||||
try:
|
||||
import glob
|
||||
@@ -39,15 +42,17 @@ SCHEMA_FILES = {
|
||||
"IFC2X3": {
|
||||
"entities": BASE_MODULE_PATH / "schema/ifc2x3_entities.json",
|
||||
"properties": BASE_MODULE_PATH / "schema/ifc2x3_properties.json",
|
||||
"types": BASE_MODULE_PATH / "schema/ifc2x3_types.json",
|
||||
},
|
||||
"IFC4": {
|
||||
"entities": BASE_MODULE_PATH / "schema/ifc4_entities.json",
|
||||
"properties": BASE_MODULE_PATH / "schema/ifc4_properties.json",
|
||||
"types": BASE_MODULE_PATH / "schema/ifc4_types.json",
|
||||
},
|
||||
}
|
||||
|
||||
db = None
|
||||
|
||||
schema_by_name = {"IFC2X3": None, "IFC4": None}
|
||||
|
||||
def get_db(version):
|
||||
global db
|
||||
@@ -65,17 +70,35 @@ def get_db(version):
|
||||
db[ifc_version][data_type] = json.load(fi)
|
||||
return db.get(version)
|
||||
|
||||
def get_schema_by_name(version):
|
||||
global schema_by_name
|
||||
if not schema_by_name[version]:
|
||||
schema_by_name[version] = ifcopenshell.ifcopenshell_wrapper.schema_by_name(version)
|
||||
return schema_by_name[version]
|
||||
|
||||
def get_entity_doc(version, entity):
|
||||
def get_entity_doc(version, entity_name, recursive=True):
|
||||
db = get_db(version)
|
||||
if db:
|
||||
return db["entities"].get(entity)
|
||||
entity = copy.deepcopy(db["entities"].get(entity_name))
|
||||
if not recursive:
|
||||
return entity
|
||||
|
||||
ifc_schema = get_schema_by_name(version)
|
||||
ifc_entity = ifc_schema.declaration_by_name(entity_name)
|
||||
ifc_supertype = ifc_entity.supertype()
|
||||
if ifc_entity.supertype():
|
||||
parent_entity = get_entity_doc(version, ifc_supertype.name(), recursive=True)
|
||||
if 'attributes' not in entity:
|
||||
entity['attributes'] = dict()
|
||||
for parent_attr in parent_entity["attributes"]:
|
||||
entity['attributes'][parent_attr] = parent_entity["attributes"][parent_attr]
|
||||
return entity
|
||||
|
||||
|
||||
def get_attribute_doc(version, entity, attribute):
|
||||
def get_attribute_doc(version, entity, attribute, recursive=True):
|
||||
db = get_db(version)
|
||||
if db:
|
||||
entity = db["entities"].get(entity)
|
||||
entity = get_entity_doc(version, entity, recursive)
|
||||
if entity:
|
||||
return entity["attributes"].get(attribute)
|
||||
|
||||
@@ -91,7 +114,6 @@ def get_property_set_doc(version, pset):
|
||||
if db:
|
||||
return db["properties"].get(pset)
|
||||
|
||||
|
||||
def get_property_doc(version, pset, prop):
|
||||
db = get_db(version)
|
||||
if db:
|
||||
@@ -99,6 +121,10 @@ def get_property_doc(version, pset, prop):
|
||||
if pset:
|
||||
return pset["properties"].get(prop)
|
||||
|
||||
def get_type_doc(version, ifc_type):
|
||||
db = get_db(version)
|
||||
if db:
|
||||
return db["types"].get(ifc_type)
|
||||
|
||||
class DocExtractor:
|
||||
def extract_ifc2x3(self):
|
||||
@@ -117,10 +143,10 @@ class DocExtractor:
|
||||
# probably due domains on the website being from 4_0
|
||||
# example (property set / github domain / website domain):
|
||||
# Pset_AirTerminalBoxPHistory IfcControlExtension IfcHvacDomain
|
||||
|
||||
self.extract_ifc2x3_property_sets_site_domains()
|
||||
self.extract_ifc2x3_entities()
|
||||
self.extract_ifc2x3_property_sets()
|
||||
self.extract_ifc2x3_types()
|
||||
|
||||
def extract_ifc2x3_property_sets_site_domains(self):
|
||||
property_sets_domains = dict()
|
||||
@@ -186,14 +212,14 @@ class DocExtractor:
|
||||
# because BeautifulSoup wrongly assume we confused
|
||||
# html code for filepath and gives warnings
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=MarkupResemblesLocatorWarning)
|
||||
warnings.simplefilter("ignore", category=MarkupResemblesLocatorWarning)
|
||||
|
||||
for html_attr in bs_tree.find_all("docattribute"):
|
||||
attr_name = html_attr["name"]
|
||||
if attr_name == "PredefinedType":
|
||||
# get references to all predefined types
|
||||
defined_type = html_attr["definedtype"]
|
||||
enum_path = xml_path.parents[2] / "Types" / defined_type / 'DocEnumeration.xml'
|
||||
enum_path = xml_path.parents[2] / "Types" / defined_type / "DocEnumeration.xml"
|
||||
with open(enum_path, "r", encoding="utf-8") as fi:
|
||||
enum_bs_tree = BeautifulSoup(fi.read(), features="lxml")
|
||||
hrefs = [i["href"] for i in enum_bs_tree.find_all("docconstant")]
|
||||
@@ -231,7 +257,6 @@ class DocExtractor:
|
||||
attr_description = attr_description.strip().rstrip(">").strip()
|
||||
entity_attrs[attr_name] = attr_description
|
||||
|
||||
|
||||
if entity_attrs:
|
||||
entities_dict[entity_name]["attributes"] = entity_attrs
|
||||
|
||||
@@ -253,7 +278,6 @@ class DocExtractor:
|
||||
def extract_ifc2x3_property_sets(self):
|
||||
property_sets_dict = dict()
|
||||
property_sets_references = dict()
|
||||
property_sets_spec_urls = dict()
|
||||
|
||||
# extract lists of properties and theirs references for each property set
|
||||
parsed_paths = [
|
||||
@@ -268,9 +292,27 @@ class DocExtractor:
|
||||
for property_set_path in glob.iglob(f"{parse_folder_path}/**/"):
|
||||
property_set_path = Path(property_set_path)
|
||||
property_set_name = property_set_path.stem
|
||||
property_set_dict = dict()
|
||||
|
||||
property_references = list()
|
||||
xml_path = property_set_path / "DocPropertySet.xml"
|
||||
md_path = property_set_path / "Documentation.md"
|
||||
|
||||
if md_path.is_file():
|
||||
with open(md_path, "r", encoding="utf-8-sig") as fi:
|
||||
# convert markdown to html for easier parsing
|
||||
html = markdown(fi.read())
|
||||
property_set_description = BeautifulSoup(html, features="lxml").find("p").text
|
||||
property_set_description = property_set_description.replace("\n", " ")
|
||||
property_set_description = property_set_description.split("HISTORY:", 1)[0]
|
||||
property_set_description = property_set_description.strip()
|
||||
property_set_dict["description"] = property_set_description
|
||||
else:
|
||||
print(
|
||||
f"WARNING. Property set {property_set_name} has no Documentation.md, "
|
||||
f"property set will be left without description."
|
||||
)
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as fi:
|
||||
bs_tree = BeautifulSoup(fi.read(), features="lxml")
|
||||
for html_attr in bs_tree.find_all("docproperty"):
|
||||
@@ -282,7 +324,8 @@ class DocExtractor:
|
||||
"https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/HTML"
|
||||
f"/psd/{property_set_domain}/{property_set_name}.xml"
|
||||
)
|
||||
property_sets_spec_urls[property_set_name] = spec_url
|
||||
property_set_dict["spec_url"] = spec_url
|
||||
property_sets_dict[property_set_name] = property_set_dict
|
||||
|
||||
# setup references look up tables to convert property hrefs to actual data paths
|
||||
references_paths_lookup = self.setup_ifc2x3_reference_lookup()
|
||||
@@ -345,16 +388,54 @@ class DocExtractor:
|
||||
for property_reference in property_sets_references[property_set_name]:
|
||||
property_name, property_dict = get_property_info_by_href(property_reference)
|
||||
properties_dict[property_name] = property_dict
|
||||
property_sets_dict[property_set_name] = {
|
||||
"properties": properties_dict,
|
||||
"spec_url": property_sets_spec_urls[property_set_name],
|
||||
}
|
||||
property_sets_dict[property_set_name]["properties"] = properties_dict
|
||||
|
||||
# export property sets data
|
||||
with open(BASE_MODULE_PATH / "schema/ifc2x3_properties.json", "w", encoding="utf-8") as fo:
|
||||
print(f"{len(property_sets_dict)} property sets parsed")
|
||||
json.dump(property_sets_dict, fo, sort_keys=True, indent=4)
|
||||
|
||||
def extract_ifc2x3_types(self):
|
||||
types_dict = dict()
|
||||
# search
|
||||
types_paths = [
|
||||
filepath for filepath in glob.iglob(f"{IFC2x3_DOCS_LOCATION}/Sections/**/Types", recursive=True)
|
||||
]
|
||||
for parse_folder_path in types_paths:
|
||||
for type_path in glob.iglob(f"{parse_folder_path}/**/"):
|
||||
type_path = Path(type_path)
|
||||
type_name = type_path.stem
|
||||
types_dict[type_name] = dict()
|
||||
md_path = type_path / "Documentation.md"
|
||||
|
||||
# utf-8-sig because of \ufeff occcurs - meaning it's utf bom encoded
|
||||
with open(md_path, "r", encoding="utf-8-sig") as fi:
|
||||
# convert markdown to html for easier parsing
|
||||
html = markdown(fi.read())
|
||||
type_description = BeautifulSoup(html, features="lxml").find("p").text
|
||||
type_description = type_description.replace("\n", " ")
|
||||
type_description = type_description.replace("\u00a0", " ")
|
||||
type_description = type_description.replace("Definition from ISO/CD 10303-46:1992: ", "")
|
||||
type_description = type_description.replace("Definition from ISO/CD 10303-42:1992 ", "")
|
||||
type_description = type_description.replace("Definition from ISO/CD 10303-42:1992: ", "")
|
||||
type_description = type_description.replace("Definition from ISO/CD 10303-41:1992: ", "")
|
||||
|
||||
type_description = type_description.strip()
|
||||
|
||||
if type_description:
|
||||
types_dict[type_name]['description'] = type_description
|
||||
|
||||
spec_url = (
|
||||
"https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/HTML/"
|
||||
f"{md_path.parents[2].name.lower()}/lexical/{type_name.lower()}.htm"
|
||||
)
|
||||
types_dict[type_name]['spec_url'] = spec_url
|
||||
|
||||
# export entities data
|
||||
with open(BASE_MODULE_PATH / "schema/ifc2x3_types.json", "w", encoding="utf-8") as fo:
|
||||
print(f"{len(types_dict)} ifc types parsed")
|
||||
json.dump(types_dict, fo, sort_keys=True, indent=4)
|
||||
|
||||
def extract_ifc4(self):
|
||||
print("Parsing data for Ifc4.0.2.1")
|
||||
if not IFC4_DOCS_LOCATION.is_dir():
|
||||
@@ -374,6 +455,7 @@ class DocExtractor:
|
||||
self.extract_ifc4_property_sets_site_domains()
|
||||
self.extract_ifc4_entities()
|
||||
self.extract_ifc4_property_sets()
|
||||
self.extract_ifc4_types()
|
||||
|
||||
def extract_ifc4_property_sets_site_domains(self):
|
||||
property_sets_domains = dict()
|
||||
@@ -434,12 +516,12 @@ class DocExtractor:
|
||||
entity_name = entity_path.stem
|
||||
entities_dict[entity_name] = dict()
|
||||
|
||||
# utf-8-sig because of \ufeff occcurs - meaning it's utf bom encoded
|
||||
md_path = entity_path / "Documentation.md"
|
||||
xml_path = entity_path / "DocEntity.xml"
|
||||
md_url_part = urllib.parse.quote(str(md_path.relative_to(Path(__file__).parent).as_posix()))
|
||||
github_md_url = f"https://github.com/buildingSMART/IFC/blob/{md_url_part}"
|
||||
|
||||
# utf-8-sig because of \ufeff occcurs - meaning it's utf bom encoded
|
||||
with open(md_path, "r", encoding="utf-8-sig") as fi:
|
||||
# convert markdown to html for easier parsing
|
||||
html = markdown(fi.read())
|
||||
@@ -459,12 +541,13 @@ class DocExtractor:
|
||||
# html code for filepath and gives warnings
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=MarkupResemblesLocatorWarning)
|
||||
|
||||
for html_attr in bs_tree.find_all("docattribute"):
|
||||
attr_name = html_attr["name"]
|
||||
if attr_name == "PredefinedType":
|
||||
# get references to all predefined types
|
||||
defined_type = html_attr["definedtype"]
|
||||
enum_path = xml_path.parents[2] / "Types" / defined_type / 'DocEnumeration.xml'
|
||||
enum_path = xml_path.parents[2] / "Types" / defined_type / "DocEnumeration.xml"
|
||||
with open(enum_path, "r", encoding="utf-8") as fi:
|
||||
enum_bs_tree = BeautifulSoup(fi.read(), features="lxml")
|
||||
hrefs = [i["href"] for i in enum_bs_tree.find_all("docconstant")]
|
||||
@@ -520,7 +603,6 @@ class DocExtractor:
|
||||
# function parses both property and quantity sets
|
||||
property_sets_dict = dict()
|
||||
property_sets_references = dict()
|
||||
property_sets_spec_urls = dict()
|
||||
|
||||
# extract lists of properties and theirs references for each property set
|
||||
parsed_paths = [
|
||||
@@ -539,10 +621,27 @@ class DocExtractor:
|
||||
for property_set_path in glob.iglob(f"{parse_folder_path}/**/"):
|
||||
property_set_path = Path(property_set_path)
|
||||
property_set_name = property_set_path.stem
|
||||
property_set_dict = dict()
|
||||
|
||||
property_references = list()
|
||||
property_quantity = property_set_path.parents[0].name == "QuantitySets"
|
||||
xml_path = property_set_path / ("DocQuantitySet.xml" if property_quantity else "DocPropertySet.xml")
|
||||
md_path = property_set_path / "Documentation.md"
|
||||
|
||||
if md_path.is_file():
|
||||
with open(md_path, "r", encoding="utf-8-sig") as fi:
|
||||
# convert markdown to html for easier parsing
|
||||
html = markdown(fi.read())
|
||||
property_set_description = BeautifulSoup(html, features="lxml").find("p").text
|
||||
property_set_description = property_set_description.replace("\n", " ")
|
||||
property_set_description = property_set_description.split("HISTORY:", 1)[0]
|
||||
property_set_description = property_set_description.strip()
|
||||
property_set_dict["description"] = property_set_description
|
||||
else:
|
||||
print(
|
||||
f"WARNING. Property set {property_set_name} has no Documentation.md, "
|
||||
f"property set will be left without description."
|
||||
)
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as fi:
|
||||
bs_tree = BeautifulSoup(fi.read(), features="lxml")
|
||||
@@ -561,10 +660,11 @@ class DocExtractor:
|
||||
spec_url = (
|
||||
"https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML"
|
||||
f"/schema/{property_set_domain}"
|
||||
f'/{"qset" if property_quantity else "pset"}'
|
||||
f"/{'qset' if property_quantity else 'pset'}"
|
||||
f"/{property_set_name.lower()}.htm"
|
||||
)
|
||||
property_sets_spec_urls[property_set_name] = spec_url
|
||||
property_set_dict["spec_url"] = spec_url
|
||||
property_sets_dict[property_set_name] = property_set_dict
|
||||
|
||||
# setup references look up tables to convert property hrefs to actual data paths
|
||||
references_paths_lookup = self.setup_ifc4_reference_lookup()
|
||||
@@ -629,25 +729,61 @@ class DocExtractor:
|
||||
for property_reference in property_sets_references[property_set_name]:
|
||||
property_name, property_dict = get_property_info_by_href(property_reference)
|
||||
properties_dict[property_name] = property_dict
|
||||
property_sets_dict[property_set_name] = {"properties": properties_dict}
|
||||
if property_set_name in property_sets_spec_urls:
|
||||
spec_url = property_sets_spec_urls[property_set_name]
|
||||
property_sets_dict[property_set_name]["spec_url"] = spec_url
|
||||
property_sets_dict[property_set_name]["properties"] = properties_dict
|
||||
|
||||
# export property sets data
|
||||
with open(BASE_MODULE_PATH / "schema/ifc4_properties.json", "w", encoding="utf-8") as fo:
|
||||
print(f"{len(property_sets_dict)} property sets parsed")
|
||||
json.dump(property_sets_dict, fo, sort_keys=True, indent=4)
|
||||
|
||||
def extract_ifc4_types(self):
|
||||
types_dict = dict()
|
||||
# search
|
||||
types_paths = [
|
||||
filepath for filepath in glob.iglob(f"{IFC4_DOCS_LOCATION}/Sections/**/Types", recursive=True)
|
||||
]
|
||||
for parse_folder_path in types_paths:
|
||||
for type_path in glob.iglob(f"{parse_folder_path}/**/"):
|
||||
type_path = Path(type_path)
|
||||
type_name = type_path.stem
|
||||
types_dict[type_name] = dict()
|
||||
md_path = type_path / "Documentation.md"
|
||||
|
||||
# utf-8-sig because of \ufeff occcurs - meaning it's utf bom encoded
|
||||
with open(md_path, "r", encoding="utf-8-sig") as fi:
|
||||
# convert markdown to html for easier parsing
|
||||
html = markdown(fi.read().replace("{ .extDef}", ""))
|
||||
type_description = BeautifulSoup(html, features="lxml").find("p").text
|
||||
type_description = type_description.replace("\n", " ")
|
||||
type_description = type_description.replace("\u00a0", " ")
|
||||
type_description = type_description.replace("{ .extDef}", "")
|
||||
type_description = type_description.replace("NOTE Definition according to ISO/CD 10303-41:1992 ", "")
|
||||
type_description = type_description.replace("Definition from ISO/CD 10303-41:1992: ", "")
|
||||
|
||||
type_description = type_description.strip()
|
||||
|
||||
if type_description:
|
||||
types_dict[type_name]['description'] = type_description
|
||||
|
||||
spec_url = (
|
||||
"https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/schema/"
|
||||
f"{md_path.parents[2].name.lower()}/lexical/{type_name.lower()}.htm"
|
||||
)
|
||||
types_dict[type_name]['spec_url'] = spec_url
|
||||
|
||||
# export entities data
|
||||
with open(BASE_MODULE_PATH / "schema/ifc4_types.json", "w", encoding="utf-8") as fo:
|
||||
print(f"{len(types_dict)} ifc types parsed")
|
||||
json.dump(types_dict, fo, sort_keys=True, indent=4)
|
||||
|
||||
def run_doc_api_examples():
|
||||
print("Entities:")
|
||||
print(get_entity_doc("IFC2X3", "IfcActionRequest"))
|
||||
print(get_entity_doc("IFC4", "IfcActionRequest"))
|
||||
print("Entities (with parent entities attributes included):")
|
||||
print(get_entity_doc("IFC2X3", "IfcWindow"))
|
||||
print(get_entity_doc("IFC4", "IfcWindow"))
|
||||
|
||||
print("Entity attributes:")
|
||||
print(get_attribute_doc("IFC2X3", "IfcActionRequest", "RequestID"))
|
||||
print(get_attribute_doc("IFC4", "IfcActionRequest", "LongDescription"))
|
||||
print("Entity attributes (with parent entities attributes included):")
|
||||
print(get_attribute_doc("IFC2X3", "IfcWindow", "OwnerHistory"))
|
||||
print(get_attribute_doc("IFC4", "IfcWindow", "OwnerHistory"))
|
||||
|
||||
print("Entity predefined types:")
|
||||
print(get_predefined_type_doc("IFC2X3", "IfcControllerType", "FLOATING"))
|
||||
@@ -661,6 +797,10 @@ def run_doc_api_examples():
|
||||
print(get_property_doc("IFC2X3", "Pset_ZoneCommon", "Category"))
|
||||
print(get_property_doc("IFC4", "Pset_ZoneCommon", "NetPlannedArea"))
|
||||
|
||||
print("Types:")
|
||||
print(get_type_doc("IFC2X3", "IfcIsothermalMoistureCapacityMeasure"))
|
||||
print(get_type_doc("IFC4", "IfcDuration"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
extractor = DocExtractor()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -28,9 +28,13 @@ class Patcher:
|
||||
self.args = args
|
||||
|
||||
def patch(self):
|
||||
tags = {}
|
||||
if self.args:
|
||||
key = self.args[0]
|
||||
else:
|
||||
key = "Tag"
|
||||
keys = {}
|
||||
for element_type in self.file.by_type("IfcTypeObject"):
|
||||
original_type = tags.get(element_type.Tag, None)
|
||||
original_type = keys.get(getattr(element_type, key), None)
|
||||
if original_type:
|
||||
for element in ifcopenshell.util.element.get_types(element_type):
|
||||
self.assign_type(element, original_type)
|
||||
@@ -38,7 +42,7 @@ class Patcher:
|
||||
ifcopenshell.util.element.replace_attribute(inverse, element_type, original_type)
|
||||
self.file.remove(element_type)
|
||||
else:
|
||||
tags[element_type.Tag] = element_type
|
||||
keys[getattr(element_type, key)] = element_type
|
||||
|
||||
def assign_type(self, related_object, relating_type):
|
||||
# This is basically a portion of the type.assign_type API which only
|
||||
+33
-1
@@ -1,3 +1,35 @@
|
||||
# ifctester
|
||||
|
||||
Author, test, and see reports from IDS audits on the command line, as a webapp, or as a library.
|
||||
With **IfcTester**, you can author and read **Information Delivery Specification** - **IDS** - files and validate your IFC models against IDS to see if your model is compliant. After the audit, you can generate reports in console, as a web page, JSON or BCF file. It works from the command line, as a web app, or as a library.
|
||||
|
||||
## How to use it
|
||||
|
||||
```
|
||||
# create new IDS
|
||||
my_ids = ids.Ids(title="My IDS")
|
||||
|
||||
# add specification to it
|
||||
my_spec = ids.Specification(name="My first specification")
|
||||
my_spec.applicability.append(ids.Entity(name="IFCWALL"))
|
||||
property = ids.Property(
|
||||
name="IsExternal",
|
||||
value="TRUE",
|
||||
propertySet="Pset_WallCommon",
|
||||
measure="IfcBoolean",
|
||||
uri="https://identifier.buildingsmart.org/uri/.../prop/LoadBearing",
|
||||
instructions="Walls need to be load bearing.")
|
||||
my_spec.requirements.append(property)
|
||||
my_ids.specifications.append(my_spec)
|
||||
|
||||
# Save such IDS to file
|
||||
result = my_ids.to_xml("SampleIDS.xml")
|
||||
|
||||
# open IFC file:
|
||||
my_ifc = ifcopenshell.open("MyIfcModel.ifc")
|
||||
|
||||
# validate IFC model against IDS requirements:
|
||||
my_ids.validate(my_ifc)
|
||||
|
||||
# show results:
|
||||
reporter.Console(my_ids).report()
|
||||
```
|
||||
@@ -882,10 +882,14 @@ class PropertyResult(Result):
|
||||
return "The property set does not contain the required property"
|
||||
elif self.reason["type"] == "MEASURE":
|
||||
return f"The data type \"{str(self.reason['actual'])}\" does not match the requirements"
|
||||
elif self.reason["type"] == "VALUE" and len(self.reason["actual"]) == 1:
|
||||
return f"The property value \"{str(self.reason['actual'][0])}\" does not match the requirements"
|
||||
elif self.reason["type"] == "VALUE":
|
||||
return f"The property values \"{str(self.reason['actual'])}\" do not match the requirements"
|
||||
if isinstance(self.reason["actual"], list):
|
||||
if len(self.reason["actual"]) == 1:
|
||||
return f"The property value \"{str(self.reason['actual'][0])}\" does not match the requirements"
|
||||
else:
|
||||
return f"The property values \"{str(self.reason['actual'])}\" do not match the requirements"
|
||||
else:
|
||||
return f"The property value \"{str(self.reason['actual'])}\" does not match the requirements"
|
||||
elif self.reason["type"] == "PROHIBITED":
|
||||
return f"The property should not have met the requirement"
|
||||
|
||||
|
||||
@@ -204,63 +204,58 @@ class Html(Json):
|
||||
return outfile.write(pystache.render(file.read(), self.results))
|
||||
|
||||
|
||||
class BcfHandler(logging.StreamHandler):
|
||||
"""Logging handler for creation of BCF report files.
|
||||
class Bcf(Reporter):
|
||||
def __init__(self, ids, with_viewpoint=True, filepath=None, group=False):
|
||||
super().__init__(ids)
|
||||
|
||||
:param project_name: defaults to "IDS Project"
|
||||
:type project_name: str, optional
|
||||
:param author: Email of the person creating the BCF report, defaults to "your@email.com"
|
||||
:type author: str, optional
|
||||
:param filepath: Path to save the BCF report, defaults to None
|
||||
:type filepath: str, optional
|
||||
:param report_valid: True if you want to list all the compliant cases as well, defaults to False
|
||||
:type report_valid: bool, optional
|
||||
from bcf.v2.bcfxml import BcfXml
|
||||
|
||||
Example::
|
||||
self.with_viewpoint = with_viewpoint
|
||||
self.group = group
|
||||
self.bcf = BcfXml()
|
||||
self.bcf.new_project()
|
||||
if not filepath:
|
||||
self.filepath=os.path.join(sys.path[0], self.ids.info['title'] + '.bcf')
|
||||
# self.bcf.filepath = sys.path[0]
|
||||
self.bcf.project.name = self.ids.info.get("title", "Untitled IDS")
|
||||
self.bcf.author = self.ids.info.get("author", "your@email.com")
|
||||
self.bcf.creation_date = datetime.datetime.today().replace(microsecond=0).isoformat()
|
||||
self.bcf.modified_date = datetime.datetime.today().replace(microsecond=0).isoformat()
|
||||
|
||||
bcf_handler = BcfHandler(
|
||||
project_name="Default IDS Project",
|
||||
author="your@email.com",
|
||||
filepath="example.bcf",
|
||||
)
|
||||
logger = logging.getLogger("IDS_Logger")
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
logger.addHandler(bcf_handler)
|
||||
"""
|
||||
def report(self):
|
||||
for specification in self.ids.specifications:
|
||||
self.report_specification(specification, save_project=False)
|
||||
self.bcf.save_project(self.filepath)
|
||||
|
||||
def __init__(self, project_name="IDS Project", author="your@email.com", filepath=None, report_valid=False):
|
||||
def report_specification(self, specification, save_project=True):
|
||||
from bcf.v2 import data as bcf
|
||||
for requirement in specification.requirements:
|
||||
if self.group:
|
||||
topic = bcf.Topic()
|
||||
topic.title = requirement.to_string("requirement")
|
||||
topic.description = ";\n".join([requirement.failed_reasons[i] + " for " + str(e.get_info()['type']) + ": " + str(e.GlobalId) for i, e in enumerate(requirement.failed_entities)])
|
||||
self.bcf.add_topic(topic)
|
||||
if self.with_viewpoint:
|
||||
self.add_viewpoint(topic, requirement)
|
||||
else:
|
||||
for i, e in enumerate(requirement.failed_entities):
|
||||
topic = bcf.Topic()
|
||||
topic.title = requirement.to_string("requirement")
|
||||
topic.description = requirement.failed_reasons[i] + " for " + str(e.get_info()['type']) + ": " + str(e.GlobalId)
|
||||
self.bcf.add_topic(topic)
|
||||
if self.with_viewpoint:
|
||||
self.add_viewpoint(topic, requirement)
|
||||
if save_project:
|
||||
self.bcf.save_project(self.filepath)
|
||||
|
||||
def add_viewpoint(self, topic, requirement):
|
||||
import numpy as np
|
||||
import ifcopenshell.util.placement
|
||||
from bcf.v2.bcfxml import BcfXml
|
||||
from bcf.v2 import data as bcf
|
||||
|
||||
logging.StreamHandler.__init__(self)
|
||||
if report_valid:
|
||||
self.setLevel(logging.INFO)
|
||||
else:
|
||||
self.setLevel(logging.ERROR)
|
||||
self.bcf = BcfXml()
|
||||
self.bcf.author = author
|
||||
self.bcf.new_project()
|
||||
self.bcf.project.name = project_name
|
||||
self.filepath = filepath
|
||||
self.bcf.edit_project()
|
||||
|
||||
def emit(self, log_content):
|
||||
"""Triggered on each use of logging with the BCF handler enabled.
|
||||
|
||||
:param log_content: default logger message
|
||||
:type log_content: string|dict
|
||||
"""
|
||||
topic = bcf.Topic()
|
||||
topic.title = log_content.msg["sentence"].split(".\n")[1]
|
||||
topic.description = log_content.msg["sentence"].split(".\n")[0]
|
||||
self.bcf.add_topic(topic)
|
||||
# try: # Add viewpoint and link to ifc object
|
||||
viewpoint = bcf.Viewpoint()
|
||||
viewpoint.perspective_camera = bcf.PerspectiveCamera()
|
||||
ifc_elem = log_content.msg["ifc_element"]
|
||||
# ifc_elem = ifc_file.by_guid(log_content.msg["guid"])
|
||||
ifc_elem = requirement.failed_entities[0]
|
||||
target_position = np.array(ifcopenshell.util.placement.get_local_placement(ifc_elem.ObjectPlacement))
|
||||
target_position = target_position[:, 3][0:3]
|
||||
camera_position = target_position + np.array((5, 5, 5))
|
||||
@@ -289,18 +284,17 @@ class BcfHandler(logging.StreamHandler):
|
||||
viewpoint.perspective_camera.camera_up_vector.y = mat[1][1]
|
||||
viewpoint.perspective_camera.camera_up_vector.z = mat[2][1]
|
||||
viewpoint.components = bcf.Components()
|
||||
c = bcf.Component()
|
||||
c.ifc_guid = log_content.msg["guid"]
|
||||
viewpoint.components.selection.append(c)
|
||||
for e in requirement.failed_entities:
|
||||
c = bcf.Component()
|
||||
c.ifc_guid = e.GlobalId
|
||||
viewpoint.components.selection.append(c)
|
||||
viewpoint.components.visibility = bcf.ComponentVisibility()
|
||||
viewpoint.components.visibility.default_visibility = True
|
||||
viewpoint.snapshot = None
|
||||
self.bcf.add_viewpoint(topic, viewpoint)
|
||||
|
||||
def flush(self):
|
||||
"""Saves the BCF report to file. Triggered at the end of the validation process."""
|
||||
if not self.filepath:
|
||||
self.filepath = os.getcwd() + r"\IDS_report.bcf"
|
||||
if not (self.filepath.endswith(".bcf") or self.filepath.endswith(".bcfzip")):
|
||||
self.filepath = self.filepath + r"\IDS_report.bcf"
|
||||
self.bcf.save_project(self.filepath)
|
||||
# add single pixel png file as a snapshot, as viewpoints without snapshots won't open in viewers
|
||||
png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x03PLTE\x00\x00\x00\xa7z=\xda\x00\x00\x00\x01tRNS\x00@\xe6\xd8f\x00\x00\x00\nIDAT\x08\xd7c`\x00\x00\x00\x02\x00\x01\xe2!\xbc3\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
snapshot_path = os.path.join(self.bcf.filepath, topic.guid, topic.guid+".png")
|
||||
snapshot = open(snapshot_path, "wb")
|
||||
snapshot.write(png)
|
||||
snapshot.close()
|
||||
viewpoint.snapshot = topic.guid+".png"
|
||||
self.bcf.add_viewpoint(topic, viewpoint)
|
||||
Reference in New Issue
Block a user