mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 04:32:23 +00:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04cf9c870b | |||
| bf9bddc867 | |||
| 20a2fda066 | |||
| f03afb20f3 | |||
| e0df22ffec | |||
| 96302a5c07 | |||
| 38afce1628 | |||
| cd5afacc97 | |||
| c2623e5dce | |||
| 42ecd7e127 | |||
| c80a7cfcc9 | |||
| 6d8f1a3980 | |||
| 5c4c61bb15 | |||
| d8af8b58bb | |||
| 3c0405249b | |||
| 7f57649d10 | |||
| fbde23870e | |||
| 7b968030ca | |||
| b5d672ba42 | |||
| efa8da83ba | |||
| 64cf7237db | |||
| cf9a94a799 | |||
| 46c1a8c8ff | |||
| ac7f2aeb62 | |||
| 9378a763f3 | |||
| 0bc83fedae | |||
| f7c03db752 | |||
| b6b8c21741 | |||
| c8af51cfd2 | |||
| 77de3820e7 | |||
| adc5bea4e9 | |||
| 1eddcb9ee0 | |||
| eae542f90f | |||
| fab0d05279 | |||
| d2e495a69f | |||
| 02058efa4d | |||
| ed79cbb1cc | |||
| 0065c5fba8 | |||
| 1ab24e8362 | |||
| 232b4ec34d | |||
| 75e6147dbc | |||
| 8d7b293381 | |||
| 19c8afb652 | |||
| 84dcee6b36 | |||
| dadcf03f55 | |||
| 413886ac6b | |||
| 377f68c020 | |||
| b718a37db2 | |||
| e21043b038 | |||
| 395598d8e3 | |||
| 813518bcb5 | |||
| 4ce7a898c9 | |||
| 78f235ea7c | |||
| bda2f50f9d | |||
| 5e5ba5b3d4 | |||
| 22b0e9dcc5 | |||
| 68420d43ea | |||
| 924e337c90 | |||
| 04599a10d2 | |||
| 84c931bb5f | |||
| 572aedb3eb | |||
| 1db86c24e3 | |||
| 74adde3030 | |||
| f68e8f0e00 | |||
| 98386984f3 | |||
| 7b35ed97ca | |||
| b3302ed679 | |||
| e12f060af8 | |||
| 6d4f47a2ca | |||
| e49ab7e0cc | |||
| ccfe7ab80a | |||
| 7b95358975 | |||
| bcc50aaadc | |||
| 30f63b7d67 | |||
| ead13e23bc | |||
| 24257d2a85 | |||
| 5c4dbb924d | |||
| 88e1b3d2f9 | |||
| f67b80c6a6 | |||
| c7fd42f2cf | |||
| bed03bf83c | |||
| 1e49400e93 | |||
| 608c0dc05d | |||
| 921dbe9dae | |||
| a7e0837f96 | |||
| 071c80f845 | |||
| 06d823b9d8 | |||
| b15e33e5e9 | |||
| 55885f7a24 | |||
| e5fd6d9152 | |||
| 4dda7e84a0 | |||
| bb6be4d52d | |||
| 01d487f585 | |||
| 67a17ddd1b | |||
| f916edce1e | |||
| b0d810199b | |||
| 515a72cec9 | |||
| 7464f7519f | |||
| f0dde4f825 | |||
| 54fefb3684 | |||
| 628fcb2999 | |||
| 22f830b053 | |||
| 579c72fc69 | |||
| 642a0f7084 | |||
| 7cd5aab1c5 | |||
| 4b15535c9c | |||
| b978da24d9 | |||
| 7132c4e9bf | |||
| 5aa6123806 | |||
| e48299cb3f | |||
| c65088a7a7 | |||
| 4f53d8c5f1 |
@@ -81,6 +81,9 @@ src/blenderbim/blenderbim/translations.py
|
||||
|
||||
# blenderbim test temp files
|
||||
src/blenderbim/test/files/temp
|
||||
src/blenderbim/test/files/basic.ifc.cache.blend
|
||||
src/blenderbim/test/files/basic.ifc.cache.sqlite
|
||||
|
||||
src/blenderbim/drawings
|
||||
src/blenderbim/layouts
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
@@ -362,7 +362,9 @@ endif
|
||||
# Provides express rule validation for ifcopenshell.validate
|
||||
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
|
||||
# Provides Brickschema functionality
|
||||
cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages
|
||||
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
|
||||
# Required for SVG to DXF conversion
|
||||
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
|
||||
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* (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
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* (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
|
||||
* 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.
|
||||
*
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -75,6 +75,10 @@ def draw_attribute(attribute, layout, copy_operator=None):
|
||||
op.data_path = attribute.path_from_id("string_value")
|
||||
if attribute.is_optional:
|
||||
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
|
||||
if attribute.name == "GlobalId":
|
||||
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
|
||||
|
||||
if copy_operator:
|
||||
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
|
||||
op.name = attribute.name
|
||||
@@ -216,39 +220,6 @@ def get_enum_items(data, prop_name, context=None):
|
||||
return items
|
||||
|
||||
|
||||
def get_obj_ifc_definition_id(context, obj, obj_type):
|
||||
if obj_type == "Object":
|
||||
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
|
||||
elif obj_type == "Material":
|
||||
return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id
|
||||
elif obj_type == "MaterialSet":
|
||||
return ifcopenshell.util.element.get_material(
|
||||
tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True
|
||||
).id()
|
||||
elif obj_type == "MaterialSetItem":
|
||||
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
|
||||
elif obj_type == "Task":
|
||||
return context.scene.BIMTaskTreeProperties.tasks[
|
||||
context.scene.BIMWorkScheduleProperties.active_task_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Cost":
|
||||
return context.scene.BIMCostProperties.cost_items[
|
||||
context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Resource":
|
||||
return context.scene.BIMResourceTreeProperties.resources[
|
||||
context.scene.BIMResourceProperties.active_resource_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Profile":
|
||||
return context.scene.BIMProfileProperties.profiles[
|
||||
context.scene.BIMProfileProperties.active_profile_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "WorkSchedule":
|
||||
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
|
||||
elif obj_type == "Group":
|
||||
prop = context.scene.BIMGroupProperties
|
||||
return prop.groups[prop.active_group_index].ifc_definition_id
|
||||
|
||||
# hack to close popup
|
||||
# https://blender.stackexchange.com/a/202576/130742
|
||||
def close_operator_panel(event):
|
||||
@@ -304,6 +275,9 @@ def draw_filter(layout, filter_groups, data, module):
|
||||
op.type = sprops.facet
|
||||
op.index = i
|
||||
op.module = module
|
||||
op = row.operator("bim.remove_filter_group", text="", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
|
||||
for j, ifc_filter in enumerate(filter_group.filters):
|
||||
if ifc_filter.type == "entity":
|
||||
@@ -350,11 +324,6 @@ def draw_filter(layout, filter_groups, data, module):
|
||||
op.index = j
|
||||
op.module = module
|
||||
|
||||
row = box.row()
|
||||
op = row.operator("bim.remove_filter_group", icon="X")
|
||||
op.index = i
|
||||
op.module = module
|
||||
|
||||
|
||||
# TODO this should move into ifcopenshell.util
|
||||
|
||||
|
||||
@@ -1164,6 +1164,9 @@ class IfcImporter:
|
||||
|
||||
return obj
|
||||
|
||||
def load_existing_meshes(self) -> None:
|
||||
self.meshes.update({m.name: m for m in bpy.data.meshes})
|
||||
|
||||
def get_representation_item_material_name(self, item):
|
||||
if not item.StyledByItem:
|
||||
return
|
||||
|
||||
@@ -94,9 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
|
||||
relating_obj=tool.Ifc.get_object(aggregate),
|
||||
related_obj=tool.Ifc.get_object(element),
|
||||
)
|
||||
|
||||
|
||||
# Removes Pset related to Linked Aggregates
|
||||
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate")
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
@@ -206,15 +206,29 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
bl_label = "Select Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
select_parts: bpy.props.BoolProperty(default=False)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.select_parts:
|
||||
return "Select Aggregate and Parts"
|
||||
else:
|
||||
return "Select Aggregate"
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) or context.active_object
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if self.select_parts:
|
||||
bpy.ops.bim.select_parts(obj=aggregate_obj.name)
|
||||
if aggregate_obj in context.selectable_objects:
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
tool.Blender.set_objects_selection(
|
||||
context,
|
||||
aggregate_obj,
|
||||
[aggregate_obj],
|
||||
clear_previous_selection=not self.select_parts,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -242,6 +256,7 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
|
||||
part_name=self.part_name,
|
||||
)
|
||||
|
||||
|
||||
class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.break_link_to_other_aggregates"
|
||||
bl_label = "Break link to other aggregates"
|
||||
@@ -256,12 +271,12 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
|
||||
return []
|
||||
|
||||
parts = ifcopenshell.util.element.get_parts(aggregate)
|
||||
|
||||
|
||||
for part in parts:
|
||||
pset = ifcopenshell.util.element.get_pset(part, 'BBIM_Linked_Aggregate')
|
||||
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate")
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
|
||||
linked_aggregate_group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
@@ -269,9 +284,10 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], product=aggregate)
|
||||
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.select_linked_aggregates"
|
||||
bl_label = "Select linked aggregates"
|
||||
@@ -284,17 +300,17 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
|
||||
return []
|
||||
if not element:
|
||||
return []
|
||||
|
||||
|
||||
linked_aggregate_group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
]
|
||||
|
||||
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
|
||||
|
||||
for rel in linked_aggregate_group[0].IsGroupedBy or []:
|
||||
for element in rel.RelatedObjects:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
|
||||
@@ -62,8 +62,12 @@ class BIM_PT_aggregate(Panel):
|
||||
row = layout.row(align=True)
|
||||
if AggregateData.data["has_relating_object"]:
|
||||
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
|
||||
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
|
||||
op.obj = context.active_object.name
|
||||
op.select_parts = False
|
||||
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.obj = context.active_object.name
|
||||
op.select_parts = True
|
||||
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.add_aggregate", icon="ADD", text="")
|
||||
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
|
||||
|
||||
@@ -120,20 +120,46 @@ class EditAttributes(bpy.types.Operator, Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GenerateGlobalId(bpy.types.Operator):
|
||||
class GenerateGlobalId(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.generate_global_id"
|
||||
bl_label = "Regenerate GlobalId"
|
||||
bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
|
||||
if index >= 0:
|
||||
global_id = context.active_object.BIMAttributeProperties.attributes[index]
|
||||
use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# using all selected objects on shift+click
|
||||
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.use_selected = True
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
if self.use_selected:
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcRoot"):
|
||||
continue
|
||||
element.GlobalId = ifcopenshell.guid.new()
|
||||
|
||||
obj = context.active_object
|
||||
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
|
||||
return {"FINISHED"}
|
||||
|
||||
props = obj.BIMAttributeProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if not element.is_a("IfcRoot"):
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.use_selected and obj in context.selected_objects:
|
||||
# guid value was already regenerated, just update the ui prop
|
||||
guid_value = element.GlobalId
|
||||
else:
|
||||
global_id = context.active_object.BIMAttributeProperties.attributes.add()
|
||||
global_id.name = "GlobalId"
|
||||
global_id.data_type = "string"
|
||||
global_id.string_value = ifcopenshell.guid.new()
|
||||
guid_value = ifcopenshell.guid.new()
|
||||
|
||||
props.attributes["GlobalId"].string_value = guid_value
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ from math import pi
|
||||
|
||||
class BIMCadProperties(PropertyGroup):
|
||||
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1)
|
||||
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
|
||||
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
|
||||
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
|
||||
gable_roof_edge_angle: bpy.props.FloatProperty(
|
||||
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
|
||||
@@ -239,8 +240,9 @@ class CadHotkey(bpy.types.Operator):
|
||||
row.prop(props, "resolution")
|
||||
|
||||
def hotkey_S_C(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
|
||||
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
|
||||
else:
|
||||
bpy.ops.bim.cad_arc_from_2_points()
|
||||
|
||||
@@ -248,13 +250,15 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_trim_extend()
|
||||
|
||||
def hotkey_S_F(self):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
|
||||
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
|
||||
else:
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
|
||||
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
|
||||
|
||||
def hotkey_S_O(self):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
@@ -270,7 +274,8 @@ class CadHotkey(bpy.types.Operator):
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
|
||||
@@ -25,15 +25,12 @@ classes = (
|
||||
operator.ExecuteIfcClash,
|
||||
operator.ExportClashSets,
|
||||
operator.ImportClashSets,
|
||||
operator.LoadIfcClashes,
|
||||
operator.SaveIfcClashes,
|
||||
operator.LoadSmartGroupsForActiveClashSet,
|
||||
operator.RemoveClashSet,
|
||||
operator.RemoveClashSource,
|
||||
operator.SelectClash,
|
||||
operator.SelectClashResults,
|
||||
operator.SelectClashSource,
|
||||
operator.SelectIfcClashResults,
|
||||
operator.SelectSmartGroup,
|
||||
operator.SelectSmartGroupedClashesPath,
|
||||
operator.SmartClashGroup,
|
||||
@@ -44,7 +41,6 @@ classes = (
|
||||
prop.BIMClashProperties,
|
||||
ui.BIM_PT_ifcclash,
|
||||
ui.BIM_PT_clash_manager,
|
||||
ui.BIM_PT_dumb_clash_manager,
|
||||
ui.BIM_PT_smart_clash_manager,
|
||||
ui.BIM_UL_clashes,
|
||||
ui.BIM_UL_clash_sets,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 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 json
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
ClashData.is_loaded = False
|
||||
|
||||
|
||||
class ClashData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = {}
|
||||
cls.data["saved_searches"] = cls.saved_searches()
|
||||
|
||||
@classmethod
|
||||
def saved_searches(cls):
|
||||
if not tool.Ifc.get():
|
||||
return []
|
||||
groups = tool.Ifc.get().by_type("IfcGroup")
|
||||
results = []
|
||||
for group in groups:
|
||||
try:
|
||||
data = json.loads(group.Description)
|
||||
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
|
||||
results.append(group)
|
||||
except:
|
||||
pass
|
||||
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
|
||||
@@ -0,0 +1,102 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 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 blf
|
||||
import gpu
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
|
||||
|
||||
class ClashDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_text(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
text = context.scene.BIMClashProperties.active_clash_text
|
||||
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
|
||||
|
||||
font_id = 0
|
||||
blf.size(font_id, 12)
|
||||
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
|
||||
color = self.addon_prefs.decorations_colour
|
||||
blf.color(font_id, *color)
|
||||
if coords_2d:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
coords_2d -= Vector((w * .5, 0))
|
||||
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
|
||||
blf.draw(font_id, text) # Set your text here
|
||||
|
||||
def draw_geometry(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
|
||||
selected_edges = []
|
||||
if selected_vertices[0] != selected_vertices[1]:
|
||||
selected_edges = [[0, 1]]
|
||||
|
||||
self.draw_batch("POINTS", selected_vertices, special_elements_color)
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, special_elements_color, selected_edges)
|
||||
@@ -23,10 +23,11 @@ import bmesh
|
||||
import logging
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
from mathutils import Matrix, Vector
|
||||
from math import radians
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.tool as tool
|
||||
from math import radians
|
||||
from mathutils import Matrix, Vector
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.clash.decorator import ClashDecorator
|
||||
|
||||
|
||||
class ExportClashSets(bpy.types.Operator):
|
||||
@@ -45,18 +46,7 @@ class ExportClashSets(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
clash_sets = []
|
||||
for clash_set in context.scene.BIMClashProperties.clash_sets:
|
||||
self.a = []
|
||||
self.b = []
|
||||
for ab in ["a", "b"]:
|
||||
for data in getattr(clash_set, ab):
|
||||
clash_source = {"file": data.name}
|
||||
if data.selector:
|
||||
clash_source["selector"] = data.selector
|
||||
clash_source["mode"] = data.mode
|
||||
getattr(self, ab).append(clash_source)
|
||||
clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b})
|
||||
clash_sets = tool.Clash.export_clash_sets()
|
||||
with open(self.filepath, "w") as destination:
|
||||
destination.write(json.dumps(clash_sets, indent=4))
|
||||
return {"FINISHED"}
|
||||
@@ -78,25 +68,34 @@ class ImportClashSets(bpy.types.Operator):
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
with open(self.filepath) as f:
|
||||
clash_sets = json.load(f)
|
||||
for clash_set in clash_sets:
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
context.scene.BIMClashProperties.clash_sets.clear()
|
||||
for clash_set in tool.Clash.get_clash_sets():
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
new.name = clash_set["name"]
|
||||
new.tolerance = clash_set["tolerance"]
|
||||
new.mode = clash_set["mode"]
|
||||
if new.mode == "intersection":
|
||||
new.tolerance = clash_set["tolerance"]
|
||||
new.check_all = clash_set["check_all"]
|
||||
elif new.mode == "collision":
|
||||
new.allow_touching = clash_set["allow_touching"]
|
||||
elif new.mode == "clearance":
|
||||
new.clearance = clash_set["clearance"]
|
||||
new.check_all = clash_set["check_all"]
|
||||
for clash_source in clash_set["a"]:
|
||||
new_source = new.a.add()
|
||||
new_source.name = clash_source["file"]
|
||||
if "selector" in clash_source:
|
||||
new_source.selector = clash_source["selector"]
|
||||
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
|
||||
new_source.mode = clash_source["mode"]
|
||||
if clash_set["b"]:
|
||||
if "b" in clash_set and clash_set["b"]:
|
||||
for clash_source in clash_set["b"]:
|
||||
new_source = new.b.add()
|
||||
new_source.name = clash_source["file"]
|
||||
if "selector" in clash_source:
|
||||
new_source.selector = clash_source["selector"]
|
||||
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
|
||||
new_source.mode = clash_source["mode"]
|
||||
tool.Clash.import_active_clashes()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -109,7 +108,6 @@ class AddClashSet(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
new = context.scene.BIMClashProperties.clash_sets.add()
|
||||
new.name = "New Clash Set"
|
||||
new.tolerance = 0.01
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -211,8 +209,11 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".json":
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
|
||||
if extension != ".bcf":
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
# TODO Temporarily until BCF support comes back
|
||||
# if extension != ".json":
|
||||
# self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
@@ -220,7 +221,12 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
from ifcclash import ifcclash
|
||||
|
||||
self.props = context.scene.BIMClashProperties
|
||||
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension != ".bcf":
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
# TODO Temporarily until BCF support comes back
|
||||
if extension != ".json":
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
|
||||
|
||||
@@ -230,7 +236,7 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
settings.logger.setLevel(logging.DEBUG)
|
||||
clasher = ifcclash.Clasher(settings)
|
||||
|
||||
if context.scene.BIMClashProperties.should_create_clash_snapshots:
|
||||
if self.props.should_create_clash_snapshots:
|
||||
|
||||
def get_viewpoint_snapshot(viewpoint):
|
||||
camera = bpy.data.objects.get("IFC Clash Camera")
|
||||
@@ -271,23 +277,13 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
|
||||
clasher.get_viewpoint_snapshot = get_viewpoint_snapshot
|
||||
|
||||
clasher.clash_sets = []
|
||||
for clash_set in context.scene.BIMClashProperties.clash_sets:
|
||||
self.a = []
|
||||
self.b = []
|
||||
for ab in ["a", "b"]:
|
||||
for data in getattr(clash_set, ab):
|
||||
clash_source = {"file": data.name}
|
||||
if data.selector:
|
||||
clash_source["selector"] = data.selector
|
||||
clash_source["mode"] = data.mode
|
||||
getattr(self, ab).append(clash_source)
|
||||
clash_set_data = {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a}
|
||||
if self.b:
|
||||
clash_set_data["b"] = self.b
|
||||
clasher.clash_sets.append(clash_set_data)
|
||||
clasher.clash_sets = tool.Clash.export_clash_sets()
|
||||
clasher.clash()
|
||||
clasher.export()
|
||||
|
||||
if extension == ".json":
|
||||
tool.Clash.load_clash_sets(self.filepath)
|
||||
tool.Clash.import_active_clashes()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -306,6 +302,7 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
# TODO refactor into new clash results system
|
||||
self.file = IfcStore.get_file()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
with open(self.filepath) as f:
|
||||
@@ -352,83 +349,6 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadIfcClashes(bpy.types.Operator):
|
||||
bl_idname = "bim.load_ifc_clashes"
|
||||
bl_label = "Load IFC Clashes"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Load the clashing IFC geometry stored in a file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
with open(self.filepath) as f:
|
||||
clash_sets_json = json.load(f)
|
||||
active_clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
|
||||
for clash_set in clash_sets_json:
|
||||
if not "clashes" in clash_set.keys():
|
||||
self.report({"WARNING"}, "No clashes found for the selected Clash Set.")
|
||||
return {"CANCELLED"}
|
||||
active_clash_set.clashes.clear()
|
||||
for clash in clash_set["clashes"].values():
|
||||
blender_clash = active_clash_set.clashes.add()
|
||||
blender_clash.a_global_id = clash["a_global_id"]
|
||||
blender_clash.b_global_id = clash["b_global_id"]
|
||||
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
||||
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
||||
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SaveIfcClashes(bpy.types.Operator):
|
||||
bl_idname = "bim.save_ifc_clashes"
|
||||
bl_label = "Save IFC Clashes"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Save the clashing IFC geometry stored in a file"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
|
||||
clash_sets_json = self.load_json()
|
||||
active_clash_set = context.scene.BIMClashProperties.active_clash_set
|
||||
self.update_clash_sets(clash_sets_json, active_clash_set)
|
||||
self.save_json(clash_sets_json)
|
||||
return {"FINISHED"}
|
||||
|
||||
def load_json(self):
|
||||
with open(self.filepath) as f:
|
||||
return json.load(f)
|
||||
|
||||
def update_clash_sets(self, clash_sets_json, active_clash_set):
|
||||
for clash_set in clash_sets_json:
|
||||
if clash_set["name"] != active_clash_set.name or "clashes" not in clash_set.keys():
|
||||
continue
|
||||
for clash in active_clash_set.clashes:
|
||||
clash_key = clash.a_global_id + "-" + clash.b_global_id
|
||||
if clash_set.get("clashes", {}).get(clash_key, None) is not None:
|
||||
clash_set["clashes"][clash_key]["status"] = clash.status
|
||||
|
||||
def save_json(self, clash_sets_json):
|
||||
with open(self.filepath, "w") as destination:
|
||||
json.dump(clash_sets_json, destination, indent=4)
|
||||
|
||||
|
||||
class SelectClash(bpy.types.Operator):
|
||||
bl_idname = "bim.select_clash"
|
||||
bl_label = "Select Clash"
|
||||
@@ -437,31 +357,29 @@ class SelectClash(bpy.types.Operator):
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
clash = context.scene.BIMClashProperties.active_clash
|
||||
self.props = context.scene.BIMClashProperties
|
||||
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
|
||||
active_clash = self.props.active_clash
|
||||
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
|
||||
|
||||
if not clash:
|
||||
return {"FINISHED"}
|
||||
|
||||
products = []
|
||||
linked_objects = []
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(clash.a_global_id))
|
||||
except:
|
||||
linked_objects.append(clash.a_name)
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(clash.b_global_id))
|
||||
except:
|
||||
linked_objects.append(clash.b_name)
|
||||
|
||||
for global_id in (clash["a_global_id"], clash["b_global_id"]):
|
||||
try:
|
||||
products.append(tool.Ifc.get().by_guid(global_id))
|
||||
except:
|
||||
pass
|
||||
|
||||
tool.Spatial.select_products(products, unhide=True)
|
||||
print(linked_objects)
|
||||
for obj_name in linked_objects:
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
else:
|
||||
print("Object not found:", obj_name)
|
||||
if context.scene.BIMClashProperties.sould_focus_on_clash:
|
||||
context_override = tool.Blender.get_viewport_context()
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.view3d.view_selected()
|
||||
ClashDecorator.install(bpy.context)
|
||||
target = Vector(clash["p1"])
|
||||
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
|
||||
self.props.p1 = clash["p1"]
|
||||
self.props.p2 = clash["p2"]
|
||||
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -33,11 +33,12 @@ from bpy.props import (
|
||||
|
||||
class ClashSource(PropertyGroup):
|
||||
name: StringProperty(name="File")
|
||||
selector: StringProperty(name="Selector")
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
mode: EnumProperty(
|
||||
items=[
|
||||
("i", "Include", "Only the selected objects are included for clashing"),
|
||||
("e", "Exclude", "All objects except the selected objects are included for clashing"),
|
||||
("a", "All Elements", "All elements will be used for clashing"),
|
||||
("i", "Include", "Only the selected elements are included for clashing"),
|
||||
("e", "Exclude", "All elements except the selected elements are included for clashing"),
|
||||
],
|
||||
name="Mode",
|
||||
)
|
||||
@@ -53,7 +54,24 @@ class Clash(PropertyGroup):
|
||||
|
||||
class ClashSet(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
tolerance: FloatProperty(name="Tolerance")
|
||||
mode: EnumProperty(
|
||||
items=[
|
||||
(
|
||||
"intersection",
|
||||
"Intersection",
|
||||
"Detect objects that protrude or pierce another object",
|
||||
"PIVOT_MEDIAN",
|
||||
1,
|
||||
),
|
||||
("collision", "Collision", "Detect touching objects with any surface collision", "PIVOT_INDIVIDUAL", 2),
|
||||
("clearance", "Clearance", "Detect objects within a proximity threshold", "PIVOT_ACTIVE", 3),
|
||||
],
|
||||
name="Mode",
|
||||
)
|
||||
tolerance: FloatProperty(name="Tolerance", default=0.002)
|
||||
clearance: FloatProperty(name="Clearance", default=0.01)
|
||||
allow_touching: BoolProperty(name="Allow Touching", default=False)
|
||||
check_all: BoolProperty(name="Check All", default=False)
|
||||
a: CollectionProperty(name="Group A", type=ClashSource)
|
||||
b: CollectionProperty(name="Group B", type=ClashSource)
|
||||
clashes: CollectionProperty(name="Clashes", type=Clash)
|
||||
@@ -78,22 +96,21 @@ class BIMClashProperties(PropertyGroup):
|
||||
smart_clash_grouping_max_distance: IntProperty(
|
||||
name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10
|
||||
)
|
||||
sould_focus_on_clash: BoolProperty(name="Show Focus Clash", default=False)
|
||||
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
|
||||
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
|
||||
active_clash_text: StringProperty(name="Active Clash Text")
|
||||
|
||||
@property
|
||||
def active_clash_set(self):
|
||||
if not self.clash_sets:
|
||||
return None
|
||||
return self.clash_sets[self.active_clash_set_index]
|
||||
if self.active_clash_set_index < len(self.clash_sets):
|
||||
return self.clash_sets[self.active_clash_set_index]
|
||||
|
||||
@property
|
||||
def active_smart_group(self):
|
||||
if not self.smart_clash_groups:
|
||||
return None
|
||||
return self.smart_clash_groups[self.active_smart_group_index]
|
||||
if self.active_smart_group_index < len(self.smart_clash_groups):
|
||||
return self.smart_clash_groups[self.active_smart_group_index]
|
||||
|
||||
@property
|
||||
def active_clash(self):
|
||||
if not self.active_clash_set:
|
||||
return None
|
||||
return self.active_clash_set.clashes[self.active_clash_index]
|
||||
if self.active_clash_index < len(self.active_clash_set.clashes):
|
||||
return self.active_clash_set.clashes[self.active_clash_index]
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim.helper
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.module.clash.data import ClashData
|
||||
|
||||
|
||||
class BIM_PT_ifcclash(Panel):
|
||||
@@ -30,6 +32,9 @@ class BIM_PT_ifcclash(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_clash_detection"
|
||||
|
||||
def draw(self, context):
|
||||
if not ClashData.is_loaded:
|
||||
ClashData.load()
|
||||
|
||||
layout = self.layout
|
||||
|
||||
scene = context.scene
|
||||
@@ -45,57 +50,83 @@ class BIM_PT_ifcclash(Panel):
|
||||
|
||||
layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index")
|
||||
|
||||
if props.active_clash_set_index < len(props.clash_sets):
|
||||
clash_set = props.active_clash_set
|
||||
if not props.active_clash_set:
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(clash_set, "name")
|
||||
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
|
||||
clash_set = props.active_clash_set
|
||||
|
||||
row = layout.row(align=True)
|
||||
row = layout.row(align=True)
|
||||
row.prop(clash_set, "name")
|
||||
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
|
||||
|
||||
row = layout.row()
|
||||
row.prop(clash_set, "mode")
|
||||
|
||||
if clash_set.mode == "intersection":
|
||||
row = layout.row()
|
||||
row.prop(clash_set, "tolerance")
|
||||
|
||||
layout.label(text="Group A:")
|
||||
row = layout.row()
|
||||
row.operator("bim.add_clash_source").group = "a"
|
||||
|
||||
for index, source in enumerate(clash_set.a):
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "name", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
op = row.operator("bim.remove_clash_source", icon="X", text="")
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "mode", text="")
|
||||
row.prop(source, "selector", text="")
|
||||
|
||||
layout.label(text="Group B:")
|
||||
row.prop(clash_set, "check_all")
|
||||
elif clash_set.mode == "collision":
|
||||
row = layout.row()
|
||||
row.operator("bim.add_clash_source").group = "b"
|
||||
|
||||
for index, source in enumerate(clash_set.b):
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "name", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
op = row.operator("bim.remove_clash_source", icon="X", text="")
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "mode", text="")
|
||||
row.prop(source, "selector", text="")
|
||||
|
||||
row.prop(clash_set, "allow_touching")
|
||||
elif clash_set.mode == "clearance":
|
||||
row = layout.row()
|
||||
row.prop(props, "should_create_clash_snapshots")
|
||||
row.prop(clash_set, "clearance")
|
||||
row = layout.row()
|
||||
row.prop(clash_set, "check_all")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD")
|
||||
row.operator("bim.add_clash_source", icon="ADD", text="").group = "a"
|
||||
|
||||
for index, source in enumerate(clash_set.a):
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.execute_ifc_clash")
|
||||
row.operator("bim.select_ifc_clash_results")
|
||||
row.prop(source, "name", text="")
|
||||
row.prop(source, "mode", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
op = row.operator("bim.remove_clash_source", icon="X", text="")
|
||||
op.index = index
|
||||
op.group = "a"
|
||||
|
||||
if source.mode != "a":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
|
||||
row.operator("bim.add_clash_source", icon="ADD", text="").group = "b"
|
||||
|
||||
for index, source in enumerate(clash_set.b):
|
||||
row = layout.row(align=True)
|
||||
row.prop(source, "name", text="")
|
||||
row.prop(source, "mode", text="")
|
||||
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
op = row.operator("bim.remove_clash_source", icon="X", text="")
|
||||
op.index = index
|
||||
op.group = "b"
|
||||
|
||||
if source.mode != "a":
|
||||
blenderbim.bim.helper.draw_filter(
|
||||
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
|
||||
)
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "should_create_clash_snapshots")
|
||||
row = layout.row()
|
||||
row.operator("bim.execute_ifc_clash")
|
||||
|
||||
row = layout.row()
|
||||
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
|
||||
|
||||
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
|
||||
row = layout.row()
|
||||
row.operator("bim.select_clash")
|
||||
|
||||
|
||||
class BIM_PT_clash_manager(Panel):
|
||||
@@ -111,29 +142,6 @@ class BIM_PT_clash_manager(Panel):
|
||||
pass
|
||||
|
||||
|
||||
class BIM_PT_dumb_clash_manager(Panel):
|
||||
bl_idname = "BIM_PT_dumb_clash_manager"
|
||||
bl_label = "Dumb Manager"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_clash_manager"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.BIMClashProperties
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.load_ifc_clashes", text="LOAD IFC CLASHES", icon="IMPORT")
|
||||
row.operator("bim.save_ifc_clashes", text="SAVE CLASH RESULTS", icon="EXPORT")
|
||||
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
|
||||
# bim.select_clash
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.select_clash", text="SELECT CLASH", icon="IMPORT")
|
||||
row.prop(props, "sould_focus_on_clash", icon="RESTRICT_VIEW_OFF")
|
||||
|
||||
|
||||
class BIM_PT_smart_clash_manager(Panel):
|
||||
bl_idname = "BIM_PT_smart_clash_manager"
|
||||
bl_label = "Smart Clash Manager"
|
||||
|
||||
@@ -293,7 +293,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
identification = active_reference[1]
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
|
||||
for reference in references:
|
||||
@@ -358,7 +358,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
break
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
if not ifc_definition_id:
|
||||
continue
|
||||
ifcopenshell.api.run(
|
||||
@@ -406,7 +406,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
classification.Location = bsdd_classification.domain_namespace_uri
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
if not ifc_definition_id:
|
||||
continue
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
|
||||
@@ -191,11 +191,11 @@ class CoveringToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_3D")
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
|
||||
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
|
||||
else:
|
||||
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
|
||||
row = cls.layout.row()
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
|
||||
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
|
||||
|
||||
@classmethod
|
||||
def draw_basic_bim_tool_interface(cls):
|
||||
|
||||
@@ -75,6 +75,9 @@ class BIM_PT_debug(Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.profile_import_ifc")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.generate_global_id")
|
||||
|
||||
row = layout.split(factor=0.5, align=True)
|
||||
row.operator("bim.create_shape_from_step_id").should_include_curves = False
|
||||
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
|
||||
|
||||
@@ -38,6 +38,7 @@ class DiffData:
|
||||
cls.data["total_deleted"] = cls.total_deleted()
|
||||
cls.data["total_changed"] = cls.total_changed()
|
||||
cls.data["changes"] = cls.changes()
|
||||
cls.data["saved_searches"] = cls.saved_searches()
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
@@ -84,3 +85,18 @@ class DiffData:
|
||||
elif element.GlobalId in diff["deleted"]:
|
||||
changes["Deleted"] = True
|
||||
return changes
|
||||
|
||||
@classmethod
|
||||
def saved_searches(cls):
|
||||
if not tool.Ifc.get():
|
||||
return []
|
||||
groups = tool.Ifc.get().by_type("IfcGroup")
|
||||
results = []
|
||||
for group in groups:
|
||||
try:
|
||||
data = json.loads(group.Description)
|
||||
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
|
||||
results.append(group)
|
||||
except:
|
||||
pass
|
||||
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
|
||||
|
||||
@@ -135,6 +135,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
bl_label = "Execute IFC Diff"
|
||||
filename_ext = ".json"
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
|
||||
@@ -162,7 +163,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
|
||||
new = ifcopenshell.open(self.props.new_file)
|
||||
|
||||
relationships = [r.relationship for r in self.props.diff_relationships]
|
||||
query = self.props.diff_filter_elements
|
||||
query = tool.Search.export_filter_query(self.props.filter_groups) or None
|
||||
|
||||
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
|
||||
ifc_diff.diff()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
@@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup):
|
||||
old_file: StringProperty(default="", name="Old IFC File")
|
||||
new_file: StringProperty(default="", name="New IFC File")
|
||||
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
|
||||
diff_filter_elements: StringProperty(default="", name="Filter")
|
||||
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
|
||||
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
|
||||
active_file: EnumProperty(
|
||||
items=[
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.diff.data import DiffData
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
import json
|
||||
|
||||
|
||||
class BIM_PT_diff(Panel):
|
||||
@@ -93,9 +93,7 @@ class BIM_PT_diff(Panel):
|
||||
remove.collection = "diff_relationships"
|
||||
remove.index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "diff_filter_elements")
|
||||
row.operator("bim.ifc_selector", icon="FILTER", text="")
|
||||
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.execute_ifc_diff")
|
||||
|
||||
@@ -41,6 +41,7 @@ classes = (
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.ContractTargetView,
|
||||
operator.ConvertSVGToDXF,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
operator.DisableAddAnnotationType,
|
||||
|
||||
@@ -188,7 +188,6 @@ def format_distance(
|
||||
feet += 1
|
||||
inches = 0
|
||||
|
||||
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches
|
||||
tx_dist = ""
|
||||
@@ -299,7 +298,9 @@ def parse_diagram_scale(camera):
|
||||
return float(numerator) / float(denominator)
|
||||
|
||||
|
||||
def ortho_view_frame(camera, margin=0.015):
|
||||
def ortho_view_frame(
|
||||
camera: bpy.types.Camera, margin: float = 0.015
|
||||
) -> tuple[float, float, float, float, float, float]:
|
||||
"""Calculates 2d bounding box of camera view area.
|
||||
|
||||
Similar to `bpy.types.Camera.view_frame`
|
||||
|
||||
@@ -44,7 +44,7 @@ import blenderbim.bim.module.drawing.helper as helper
|
||||
import blenderbim.bim.export_ifc
|
||||
from blenderbim.bim.module.drawing.decoration import CutDecorator
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
|
||||
from typing import NamedTuple, List
|
||||
from typing import NamedTuple, List, Union
|
||||
from lxml import etree
|
||||
from mathutils import Vector, Color, Matrix
|
||||
from timeit import default_timer as timer
|
||||
@@ -1345,7 +1345,7 @@ class SelectAllDrawings(bpy.types.Operator):
|
||||
bl_label = "Select All Drawings"
|
||||
view: bpy.props.StringProperty()
|
||||
bl_description = "Select all drawings in the drawing list.\n\n" + "SHIFT+CLICK to deselect all drawings"
|
||||
select_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
|
||||
select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# deselect all drawings on shift+click
|
||||
@@ -1451,10 +1451,6 @@ class ActivateModel(bpy.types.Operator):
|
||||
|
||||
|
||||
class ActivateDrawing(bpy.types.Operator):
|
||||
"""
|
||||
Activates the selected drawing view
|
||||
"""
|
||||
|
||||
bl_idname = "bim.activate_drawing"
|
||||
bl_label = "Activate Drawing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -2689,3 +2685,50 @@ class AddReferenceImage(bpy.types.Operator, Operator):
|
||||
)
|
||||
tool.Style.reload_material_from_ifc(material)
|
||||
tool.Geometry.record_object_materials(obj)
|
||||
|
||||
|
||||
class ConvertSVGToDXF(bpy.types.Operator):
|
||||
bl_idname = "bim.convert_svg_to_dxf"
|
||||
bl_label = "Convert SVG to DXF"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
view: bpy.props.StringProperty()
|
||||
bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings"
|
||||
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# convert all drawings on shift+click
|
||||
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.open_all = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
if self.convert_all:
|
||||
drawings = [
|
||||
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
|
||||
]
|
||||
else:
|
||||
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
|
||||
|
||||
drawing_uris: list[Path] = []
|
||||
drawings_not_found: list[str] = []
|
||||
|
||||
for drawing in drawings:
|
||||
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
|
||||
if drawing_uri is None or not os.path.exists(drawing_uri):
|
||||
drawings_not_found.append(drawing.Name)
|
||||
else:
|
||||
drawing_uris.append(Path(drawing_uri))
|
||||
|
||||
if drawings_not_found:
|
||||
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
|
||||
"\n".join(drawings_not_found)
|
||||
)
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
for drawing_uri in drawing_uris:
|
||||
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
|
||||
|
||||
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -268,6 +268,11 @@ class BIM_PT_drawings(Panel):
|
||||
col = row.column()
|
||||
col.alignment = "RIGHT"
|
||||
|
||||
convert_to_dxf = row.row(align=True)
|
||||
op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA")
|
||||
op.view = active_drawing.name
|
||||
convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0
|
||||
|
||||
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
|
||||
|
||||
open_drawing_button = row.row(align=True)
|
||||
|
||||
@@ -250,7 +250,7 @@ class AnnotationToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_3D")
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
row.operator("bim.launch_annotation_type_manager", icon="LIGHTPROBE_GRID", text="")
|
||||
row.operator("bim.launch_annotation_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ import bmesh
|
||||
import mathutils
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
import blenderbim.tool as tool
|
||||
from math import pi, pow
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from typing import Union
|
||||
|
||||
|
||||
class Helper:
|
||||
def __init__(self, file):
|
||||
def __init__(self, file: ifcopenshell.file):
|
||||
self.file = file
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
|
||||
@@ -34,7 +36,7 @@ class Helper:
|
||||
# edge that shares a single vertex only with that face to find the extrusion
|
||||
# edge. A face with the normal facing down is prioritised. A limited
|
||||
# dissolve ensure that faces are quads and not tris.
|
||||
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
|
||||
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
@@ -115,7 +117,9 @@ class Helper:
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
|
||||
def auto_detect_arbitrary_profile_with_voids(
|
||||
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
|
||||
) -> Union[tuple, dict]:
|
||||
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCARCINDEX" in group.name:
|
||||
@@ -138,28 +142,31 @@ class Helper:
|
||||
total_groups = 0
|
||||
is_circle = False
|
||||
for group_type, group_indices in groups.items():
|
||||
for group_index in group_indices:
|
||||
if group_index in vert[deform_layer]:
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
total_groups += 0
|
||||
if not group_indices:
|
||||
continue
|
||||
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
|
||||
if not is_special:
|
||||
continue
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
total_groups += 0
|
||||
if total_groups > 1: # A vert can only belong to one group
|
||||
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
|
||||
elif is_circle:
|
||||
pass # Circles are allowed to be unclosed
|
||||
pass # Circles are allowed to be unclosed
|
||||
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
|
||||
return (False, "UNCLOSED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = set(bm.edges)
|
||||
@@ -269,7 +276,7 @@ class Helper:
|
||||
inner_loops.remove(outer_loop)
|
||||
|
||||
# Copy vectors to prevent random data mangling after bmesh is freed.
|
||||
points = [Vector(list(v.co)) for v in bm.verts]
|
||||
points = [v.co.copy() for v in bm.verts]
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
|
||||
@@ -908,7 +908,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
index += 1
|
||||
pset = tool.Ifc.get().by_id(pset['id'])
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
@@ -976,26 +976,23 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if parts:
|
||||
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
|
||||
add_linked_aggregate_pset(element, index)
|
||||
index +=1
|
||||
index += 1
|
||||
for part in parts:
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
select_objects_and_add_data(part)
|
||||
else:
|
||||
add_linked_aggregate_pset(part, index)
|
||||
index += 1
|
||||
|
||||
|
||||
obj = tool.Ifc.get_object(part)
|
||||
obj.select_set(True)
|
||||
|
||||
|
||||
|
||||
def add_linked_aggregate_pset(part, index):
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
|
||||
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
|
||||
)
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
@@ -1029,21 +1026,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
if r.is_a("IfcRelAssignsToGroup")
|
||||
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
|
||||
][0]
|
||||
|
||||
|
||||
number = len(group_elements) - 1
|
||||
number = f"{number:02d}"
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
pattern1 = r'_\d'
|
||||
pattern1 = r"_\d"
|
||||
if re.findall(pattern1, new_obj.name):
|
||||
split_name = new_obj.name.split("_")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
continue
|
||||
pattern2 = r'\.\d{3}'
|
||||
pattern2 = r"\.\d{3}"
|
||||
if re.findall(pattern2, new_obj.name):
|
||||
split_name = new_obj.name.split(".")
|
||||
new_obj.name = split_name[0] + "_" + number
|
||||
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1065,7 +1061,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
custom_incremental_naming_for_element_assembly(old_to_new)
|
||||
|
||||
|
||||
# Recreate aggregate relationship
|
||||
for old in old_to_new.keys():
|
||||
if old.is_a("IfcElementAssembly"):
|
||||
@@ -1075,17 +1071,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
|
||||
|
||||
return old_to_new
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_max_index(parts):
|
||||
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
|
||||
index = [i['Index'] for i in psets if i]
|
||||
index = [i["Index"] for i in psets if i]
|
||||
if len(index) > 0:
|
||||
index = max(index)
|
||||
return index
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
@@ -1134,9 +1128,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names[group] = {}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(element).name
|
||||
|
||||
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
for part in parts:
|
||||
@@ -1144,20 +1138,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
original_names | get_original_names(part)
|
||||
else:
|
||||
try:
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
|
||||
except:
|
||||
continue
|
||||
index = pset['Index']
|
||||
continue
|
||||
index = pset["Index"]
|
||||
original_names[group][index] = tool.Ifc.get_object(part).name
|
||||
|
||||
|
||||
return original_names
|
||||
|
||||
def set_original_name(obj, original_names):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
if ifcopenshell.util.element.get_parts(
|
||||
element
|
||||
): # if element has parts it means it is the base of and aggregate or sub-aggregate
|
||||
aggregate = element
|
||||
|
||||
|
||||
group = [
|
||||
r.RelatingGroup
|
||||
for r in getattr(aggregate, "HasAssignments", []) or []
|
||||
@@ -1166,18 +1162,16 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
]
|
||||
if not group:
|
||||
return
|
||||
|
||||
|
||||
group = group[0].id()
|
||||
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
|
||||
index = pset['Index']
|
||||
|
||||
index = pset["Index"]
|
||||
|
||||
try:
|
||||
obj.name = original_names[group][index]
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
|
||||
def get_element_assembly(element):
|
||||
if element.is_a("IfcElementAssembly"):
|
||||
@@ -1263,9 +1257,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
selected_matrix = selected_obj.matrix_world
|
||||
object_duplicate = tool.Ifc.get_object(element)
|
||||
duplicate_matrix = object_duplicate.matrix_world.decompose()
|
||||
|
||||
|
||||
original_names = get_original_names(element)
|
||||
|
||||
|
||||
delete_objects(element)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
@@ -1280,24 +1274,24 @@ class RefreshLinkedAggregate(bpy.types.Operator):
|
||||
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
|
||||
new_obj_matrix = new_base_matrix @ matrix_diff
|
||||
new_obj.matrix_world = new_obj_matrix
|
||||
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
if element_aggregate and new[0].is_a("IfcElementAssembly"):
|
||||
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
|
||||
|
||||
if not new_aggregate:
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(element_aggregate),
|
||||
related_obj=tool.Ifc.get_object(new[0]),
|
||||
)
|
||||
|
||||
for old, new in old_to_new.items():
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
set_original_name(new_obj, original_names)
|
||||
|
||||
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
operator_time = time() - refresh_start_time
|
||||
|
||||
@@ -212,7 +212,7 @@ class BIM_PT_representation_items(Panel):
|
||||
if props.is_editing_item_style:
|
||||
# NOTE: we currently support 1 item having just 1 style
|
||||
# when IfcStyledItem can actually have multiple styles
|
||||
row.prop(props, "representation_item_style", icon="SHADING_RENDERED", text="")
|
||||
prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="")
|
||||
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
|
||||
else:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import os
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.doc
|
||||
import ifcopenshell.util.schema
|
||||
import blenderbim.tool as tool
|
||||
|
||||
@@ -109,29 +110,41 @@ class MaterialsData:
|
||||
def active_styles(cls):
|
||||
props = bpy.context.scene.BIMMaterialProperties
|
||||
results = []
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
material = props.materials[props.active_material_index].ifc_definition_id
|
||||
if not material:
|
||||
return results
|
||||
material = tool.Ifc.get().by_id(material)
|
||||
for definition in material.HasRepresentation:
|
||||
for representation in definition.Representations:
|
||||
if not representation.is_a("IfcStyledRepresentation"):
|
||||
if not props.materials or props.active_material_index >= len(props.materials):
|
||||
return results
|
||||
|
||||
material = props.materials[props.active_material_index].ifc_definition_id
|
||||
if not material:
|
||||
return results
|
||||
|
||||
material = tool.Ifc.get_entity_by_id(material)
|
||||
|
||||
# NOTE: it's possible that data will be refreshed during material removal
|
||||
# (when it will try to fetch active material type and will reach for material_types)
|
||||
# and props.materials[i].ifc_definition_id will contain already removed id
|
||||
if not material or not material.is_a("IfcMaterial"):
|
||||
return results
|
||||
|
||||
for definition in material.HasRepresentation:
|
||||
for representation in definition.Representations:
|
||||
if not representation.is_a("IfcStyledRepresentation"):
|
||||
continue
|
||||
context = representation.ContextOfItems
|
||||
for item in representation.Items:
|
||||
if not item.is_a("IfcStyledItem"):
|
||||
continue
|
||||
context = representation.ContextOfItems
|
||||
for item in representation.Items:
|
||||
if not item.is_a("IfcStyledItem"):
|
||||
continue
|
||||
for style in item.Styles:
|
||||
if style.is_a("IfcSurfaceStyle"):
|
||||
results.append({
|
||||
for style in item.Styles:
|
||||
if style.is_a("IfcSurfaceStyle"):
|
||||
results.append(
|
||||
{
|
||||
"context_type": context.ContextType,
|
||||
"context_identifier": getattr(context, "ContextIdentifier", ""),
|
||||
"target_view": getattr(context, "TargetView", ""),
|
||||
"name": style.Name or "Unnamed",
|
||||
"id": style.id(),
|
||||
"context_id": context.id(),
|
||||
})
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_material"
|
||||
bl_label = "Add Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Material Name")
|
||||
name: bpy.props.StringProperty(default="Default")
|
||||
|
||||
def invoke(self, context, event):
|
||||
|
||||
@@ -23,6 +23,7 @@ from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from typing import Union
|
||||
|
||||
|
||||
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
|
||||
@@ -35,15 +36,6 @@ def transparent_color(color, alpha=0.1):
|
||||
return color
|
||||
|
||||
|
||||
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
|
||||
"""returns tuple boolean (whether vertex is in any of the groups)
|
||||
and related group index"""
|
||||
for group_index in vertex[deform_layer].keys():
|
||||
if group_index in groups:
|
||||
return True, group_index
|
||||
return False, None
|
||||
|
||||
|
||||
class ProfileDecorator:
|
||||
installed = None
|
||||
|
||||
@@ -148,12 +140,12 @@ class ProfileDecorator:
|
||||
# deform_layer is None if there are no verts assigned to vertex groups
|
||||
# even if there are vertex groups in the obj.vertex_groups
|
||||
if deform_layer:
|
||||
is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups)
|
||||
is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups)
|
||||
if is_arc:
|
||||
arcs.setdefault(group_index, []).append(vertex)
|
||||
special_vertex_indices[vertex.index] = group_index
|
||||
|
||||
is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups)
|
||||
is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups)
|
||||
if is_circle:
|
||||
circles.setdefault(group_index, []).append(vertex)
|
||||
special_vertex_indices[vertex.index] = group_index
|
||||
|
||||
@@ -20,9 +20,12 @@ import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.type
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.core.type
|
||||
import blenderbim.core.geometry
|
||||
@@ -698,9 +701,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
new_footprint = ifcopenshell.api.run(
|
||||
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
|
||||
@@ -516,7 +516,7 @@ class BimToolUI:
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
else:
|
||||
row.label(text="No Construction Type", icon="FILE_3D")
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
|
||||
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
|
||||
else:
|
||||
if AuthoringData.data["ifc_element_type"]:
|
||||
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
|
||||
@@ -529,7 +529,7 @@ class BimToolUI:
|
||||
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
|
||||
else:
|
||||
row.label(text="No Element Types Found", icon="ERROR")
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
|
||||
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
|
||||
|
||||
@classmethod
|
||||
def draw_basic_bim_tool_interface(cls):
|
||||
|
||||
@@ -113,6 +113,10 @@ class CreateProject(bpy.types.Operator):
|
||||
if tool.Blender.is_default_scene():
|
||||
for obj in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj)
|
||||
for mesh in bpy.data.meshes:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
for mat in bpy.data.materials:
|
||||
bpy.data.materials.remove(mat)
|
||||
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
|
||||
tool.Blender.register_toolbar()
|
||||
|
||||
@@ -856,7 +860,10 @@ class LinkIfc(bpy.types.Operator):
|
||||
continue
|
||||
new = context.scene.BIMProjectProperties.links.add()
|
||||
if self.use_relative_path:
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
|
||||
try:
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
|
||||
except:
|
||||
pass # Perhaps on another drive or something
|
||||
new.name = filepath
|
||||
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
|
||||
print(f"Finished linking {len(files)} IFCs", time.time() - start)
|
||||
@@ -899,11 +906,11 @@ class UnloadLink(bpy.types.Operator):
|
||||
if filepath.suffix.lower() == ".ifc":
|
||||
filepath = filepath.with_suffix(".ifc.cache.blend")
|
||||
|
||||
for collection in context.scene.collection.children:
|
||||
for collection in context.scene.collection.children[:]:
|
||||
if collection.library and Path(collection.library.filepath) == filepath:
|
||||
context.scene.collection.children.unlink(collection)
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
for scene in bpy.data.scenes:
|
||||
for scene in bpy.data.scenes[:]:
|
||||
if scene.library and Path(scene.library.filepath) == filepath:
|
||||
bpy.data.scenes.remove(scene)
|
||||
|
||||
@@ -1008,24 +1015,24 @@ class ToggleLinkSelectability(bpy.types.Operator):
|
||||
bl_label = "Toggle Link Selectability"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Toggle selectability"
|
||||
link: bpy.props.StringProperty()
|
||||
link: bpy.props.StringProperty(name="Linked IFC Filepath")
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMProjectProperties
|
||||
link = props.links.get(self.link)
|
||||
self.filepath = self.link
|
||||
if not os.path.isabs(self.filepath):
|
||||
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
|
||||
if not os.path.isabs(self.link):
|
||||
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
|
||||
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
|
||||
for collection in self.get_linked_collections():
|
||||
collection.hide_select = not collection.hide_select
|
||||
link.is_selectable = not collection.hide_select
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_linked_collections(self):
|
||||
def get_linked_collections(self) -> list[bpy.types.Collection]:
|
||||
return [
|
||||
c
|
||||
for c in bpy.data.collections
|
||||
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
|
||||
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
|
||||
]
|
||||
|
||||
|
||||
@@ -1034,15 +1041,15 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
||||
bl_label = "Toggle Link Visibility"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
||||
link: bpy.props.StringProperty()
|
||||
mode: bpy.props.StringProperty()
|
||||
link: bpy.props.StringProperty(name="Linked IFC Filepath")
|
||||
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMProjectProperties
|
||||
link = props.links.get(self.link)
|
||||
self.filepath = self.link
|
||||
if not os.path.isabs(self.filepath):
|
||||
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
|
||||
if not os.path.isabs(self.link):
|
||||
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
|
||||
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
|
||||
if self.mode == "WIREFRAME":
|
||||
self.toggle_wireframe(link)
|
||||
elif self.mode == "VISIBLE":
|
||||
@@ -1063,25 +1070,17 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
||||
|
||||
def toggle_visibility(self, link):
|
||||
linked_collections = self.get_linked_collections()
|
||||
queue = [bpy.context.view_layer.layer_collection]
|
||||
layer_collection = None
|
||||
|
||||
while queue:
|
||||
layer = queue.pop()
|
||||
if layer.collection in linked_collections:
|
||||
layer_collection = layer
|
||||
break
|
||||
queue.extend(list(layer.children))
|
||||
|
||||
if layer_collection:
|
||||
layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections)
|
||||
for layer_collection in layer_collections.values():
|
||||
layer_collection.exclude = not layer_collection.exclude
|
||||
link.is_hidden = layer_collection.exclude
|
||||
|
||||
def get_linked_collections(self):
|
||||
def get_linked_collections(self) -> list[bpy.types.Collection]:
|
||||
return [
|
||||
c
|
||||
for c in bpy.data.collections
|
||||
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
|
||||
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
|
||||
]
|
||||
|
||||
|
||||
@@ -1295,6 +1294,8 @@ class LoadLinkedProject(bpy.types.Operator):
|
||||
if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris
|
||||
self.process_occurrence(shape)
|
||||
if not iterator.next():
|
||||
if not chunked_verts:
|
||||
break
|
||||
mats = np.concatenate(chunked_materials)
|
||||
midx = np.concatenate(chunked_material_ids)
|
||||
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
|
||||
|
||||
@@ -85,7 +85,6 @@ class EnablePsetEditing(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.props = get_pset_props(context, self.obj, self.obj_type)
|
||||
self.props.properties.clear()
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
|
||||
|
||||
if self.pset_id:
|
||||
pset = tool.Ifc.get().by_id(self.pset_id)
|
||||
@@ -250,7 +249,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator):
|
||||
props = get_pset_props(context, self.obj, self.obj_type)
|
||||
if props.active_pset_id:
|
||||
pset = tool.Ifc.get().by_id(props.active_pset_id)
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
|
||||
if tool.Pset.is_pset_empty(pset):
|
||||
ifcopenshell.api.run(
|
||||
"pset.remove_pset", tool.Ifc.get(), product=tool.Ifc.get().by_id(ifc_definition_id), pset=pset
|
||||
@@ -272,7 +271,7 @@ class EditPset(bpy.types.Operator, Operator):
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = get_pset_props(context, self.obj, self.obj_type)
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
properties = {}
|
||||
|
||||
@@ -343,7 +342,7 @@ class RemovePset(bpy.types.Operator, Operator):
|
||||
pset_name = tool.Ifc.get().by_id(self.pset_id).Name
|
||||
for obj in objects:
|
||||
props = get_pset_props(context, obj, self.obj_type)
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
|
||||
if pset:
|
||||
@@ -374,7 +373,7 @@ class AddQto(bpy.types.Operator, Operator):
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
props = get_pset_props(context, self.obj, self.obj_type)
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
bpy.ops.bim.enable_pset_editing(
|
||||
pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
|
||||
|
||||
@@ -25,8 +25,16 @@ from shapely.geometry import Polygon
|
||||
from shapely.ops import unary_union
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim
|
||||
from typing import Literal, Union, Optional
|
||||
|
||||
|
||||
AxisType = Literal["x", "y", "z"]
|
||||
VectorTuple = tuple[float, float, float]
|
||||
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
|
||||
|
||||
|
||||
class QtoCalculator:
|
||||
@@ -45,7 +53,7 @@ class QtoCalculator:
|
||||
else:
|
||||
self.mapping_dict[key][item] = None
|
||||
|
||||
def calculate_quantity(self, qto_name, quantity_name, obj):
|
||||
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
"""calculates the value of the quantity in the project units"""
|
||||
string = "self.mapping_dict[qto_name][quantity_name](obj"
|
||||
if isinstance(mapper[qto_name][quantity_name], dict):
|
||||
@@ -54,11 +62,13 @@ class QtoCalculator:
|
||||
args = ""
|
||||
string += args
|
||||
string += ")"
|
||||
value = eval(string)
|
||||
value: float = eval(string)
|
||||
|
||||
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
|
||||
|
||||
def guess_quantity(self, prop_name, alternative_prop_names, obj):
|
||||
def guess_quantity(
|
||||
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
|
||||
) -> Union[float, None]:
|
||||
"""guess the value of the quantity by name, returns the value in the project units"""
|
||||
prop_name = prop_name.lower()
|
||||
alternative_prop_names = [p.lower() for p in alternative_prop_names]
|
||||
@@ -89,7 +99,7 @@ class QtoCalculator:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
unit_type_keywords = {
|
||||
unit_type_keywords: dict[str, QuanityTypes] = {
|
||||
"length": "Q_LENGTH",
|
||||
"width": "Q_LENGTH",
|
||||
"height": "Q_LENGTH",
|
||||
@@ -102,10 +112,10 @@ class QtoCalculator:
|
||||
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
|
||||
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
|
||||
|
||||
def get_units(self, o, vg_index):
|
||||
def get_units(self, o: bpy.types.Object, vg_index: int) -> int:
|
||||
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
|
||||
|
||||
def get_linear_length(self, o):
|
||||
def get_linear_length(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the length of the longest edge of the object bounding box
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -116,7 +126,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x, y, z)
|
||||
|
||||
def get_length(self, o, vg_index=None, main_axis: str = ""):
|
||||
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
|
||||
if vg_index is None:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
@@ -141,23 +151,23 @@ class QtoCalculator:
|
||||
length += self.get_edge_distance(o, e)
|
||||
return length
|
||||
|
||||
def get_stair_length(self, obj):
|
||||
def get_stair_length(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj)
|
||||
height = self.get_height(obj)
|
||||
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
|
||||
return stair_length
|
||||
|
||||
def get_net_stair_area(self, obj):
|
||||
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
|
||||
return OBB_net_footprint_area
|
||||
|
||||
def get_gross_stair_area(self, obj):
|
||||
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
|
||||
OBB_obj = self.get_OBB_object(obj)
|
||||
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
|
||||
return OBB_gross_footprint_area
|
||||
|
||||
def get_parametric_axis(self, obj):
|
||||
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
|
||||
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
|
||||
if relating_type:
|
||||
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
||||
@@ -172,7 +182,7 @@ class QtoCalculator:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_covering_gross_area(self, obj):
|
||||
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -181,7 +191,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_gross_footprint_area(obj)
|
||||
|
||||
def get_covering_net_area(self, obj):
|
||||
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_net_footprint_area(obj)
|
||||
@@ -190,7 +200,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_net_footprint_area(obj)
|
||||
|
||||
def get_covering_width(self, obj):
|
||||
def get_covering_width(self, obj: bpy.types.Object) -> float:
|
||||
get_parametric_axis = self.get_parametric_axis(obj)
|
||||
if not get_parametric_axis:
|
||||
return self.get_height(obj)
|
||||
@@ -199,7 +209,7 @@ class QtoCalculator:
|
||||
elif get_parametric_axis == "AXIS3":
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_width(self, o):
|
||||
def get_width(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the width of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -209,7 +219,7 @@ class QtoCalculator:
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
return min(x, y)
|
||||
|
||||
def get_height(self, o):
|
||||
def get_height(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the height of the object bounding box
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -217,32 +227,32 @@ class QtoCalculator:
|
||||
"""
|
||||
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
def get_opening_height(self, obj):
|
||||
def get_opening_height(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_width(obj)
|
||||
else:
|
||||
return self.get_height(obj)
|
||||
|
||||
def get_opening_depth(self, obj):
|
||||
def get_opening_depth(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_height(obj)
|
||||
else:
|
||||
return self.get_width(obj)
|
||||
|
||||
def get_opening_mapping_area(self, obj):
|
||||
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
|
||||
if self.is_opening_horizontal(obj):
|
||||
return self.get_net_footprint_area(obj)
|
||||
else:
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
def get_finish_ceiling_height(self, obj):
|
||||
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
space_height = self.get_height(obj)
|
||||
floor_height = self.get_finish_floor_height(obj)
|
||||
ceiling_height = self.get_ceiling_height(obj)
|
||||
finish_ceiling_height = space_height - floor_height - ceiling_height
|
||||
return finish_ceiling_height
|
||||
|
||||
def get_finish_floor_height(self, obj):
|
||||
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_floor_height = 0
|
||||
@@ -258,7 +268,7 @@ class QtoCalculator:
|
||||
|
||||
return finish_floor_height
|
||||
|
||||
def get_ceiling_height(self, obj):
|
||||
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
finish_ceiling_height = 0
|
||||
@@ -274,7 +284,7 @@ class QtoCalculator:
|
||||
|
||||
return finish_ceiling_height
|
||||
|
||||
def get_net_perimeter(self, o):
|
||||
def get_net_perimeter(self, o: bpy.types.Object) -> float:
|
||||
parsed_edges = []
|
||||
shared_edges = []
|
||||
perimeter = 0
|
||||
@@ -289,7 +299,7 @@ class QtoCalculator:
|
||||
perimeter -= self.get_edge_key_distance(o, edge_key)
|
||||
return perimeter
|
||||
|
||||
def get_gross_perimeter(self, o):
|
||||
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(o)
|
||||
mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("GrossObj", mesh)
|
||||
@@ -297,15 +307,15 @@ class QtoCalculator:
|
||||
self.delete_obj(gross_obj)
|
||||
return gross_perimeter
|
||||
|
||||
def get_space_net_perimeter(self, obj):
|
||||
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
pass
|
||||
|
||||
def get_rectangular_perimeter(self, obj):
|
||||
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
|
||||
length = self.get_length(obj, main_axis="x")
|
||||
height = self.get_height(obj)
|
||||
return (length + height) * 2
|
||||
|
||||
def get_lowest_polygons(self, o):
|
||||
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
lowest_polygons = []
|
||||
lowest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -321,7 +331,7 @@ class QtoCalculator:
|
||||
lowest_z = z
|
||||
return lowest_polygons
|
||||
|
||||
def get_highest_polygons(self, o):
|
||||
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
|
||||
highest_polygons = []
|
||||
highest_z = None
|
||||
for polygon in o.data.polygons:
|
||||
@@ -337,13 +347,13 @@ class QtoCalculator:
|
||||
highest_z = z
|
||||
return highest_polygons
|
||||
|
||||
def get_edge_key_distance(self, obj, edge_key):
|
||||
def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
|
||||
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
|
||||
|
||||
def get_edge_distance(self, obj, edge):
|
||||
def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
|
||||
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
|
||||
|
||||
def get_net_floor_area(self, obj):
|
||||
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_footprint_area(obj)
|
||||
@@ -359,7 +369,7 @@ class QtoCalculator:
|
||||
|
||||
return total_net_floor_area
|
||||
|
||||
def get_gross_ceiling_area(self, obj):
|
||||
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_top_area(obj)
|
||||
@@ -375,7 +385,7 @@ class QtoCalculator:
|
||||
|
||||
return total_gross_ceiling_area
|
||||
|
||||
def get_net_ceiling_area(self, obj):
|
||||
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_net_top_area(obj)
|
||||
@@ -395,7 +405,7 @@ class QtoCalculator:
|
||||
|
||||
return total_net_ceiling_area
|
||||
|
||||
def get_space_net_volume(self, obj):
|
||||
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
|
||||
decompositions = self.get_obj_decompositions(obj)
|
||||
if not decompositions:
|
||||
return self.get_gross_volume(obj)
|
||||
@@ -410,7 +420,7 @@ class QtoCalculator:
|
||||
|
||||
return total_space_net_volume
|
||||
|
||||
def get_net_footprint_area(self, o):
|
||||
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -421,7 +431,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_gross_footprint_area(self, o):
|
||||
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
|
||||
|
||||
:param blender-object o: blender object
|
||||
@@ -437,7 +447,7 @@ class QtoCalculator:
|
||||
self.delete_mesh(mesh)
|
||||
return gross_footprint_area
|
||||
|
||||
def get_net_roofprint_area(self, o):
|
||||
def get_net_roofprint_area(self, o: bpy.types.Object) -> float:
|
||||
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
|
||||
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
|
||||
|
||||
@@ -449,7 +459,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_side_area(self, o):
|
||||
def get_side_area(self, o: bpy.types.Object) -> float:
|
||||
# There are a few dumb options for this, but this seems the dumbest
|
||||
# until I get more practical experience on what works best.
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
@@ -457,8 +467,7 @@ class QtoCalculator:
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
return max(x * z, y * z)
|
||||
|
||||
def get_cross_section_area(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
@@ -474,7 +483,7 @@ class QtoCalculator:
|
||||
return area
|
||||
# TODO handle other types of sections, and then fall back to mesh parsing
|
||||
|
||||
def get_gross_surface_area(self, o, vg_index=None):
|
||||
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
|
||||
if vg_index is None:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_surface_area(o)
|
||||
@@ -492,29 +501,29 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def get_net_surface_area(self, obj):
|
||||
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
return self.get_mesh_area(obj.data)
|
||||
|
||||
def get_mesh_area(self, mesh):
|
||||
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
|
||||
area = 0
|
||||
for polygon in mesh.polygons:
|
||||
area += polygon.area
|
||||
return area
|
||||
|
||||
def is_polygon_in_vg(self, polygon, vertices_in_vg):
|
||||
def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
|
||||
for v in polygon.vertices:
|
||||
if v not in vertices_in_vg:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_net_volume(self, o):
|
||||
def get_net_volume(self, o: bpy.types.Object) -> float:
|
||||
o_mesh = bmesh.new()
|
||||
o_mesh.from_mesh(o.data)
|
||||
volume = o_mesh.calc_volume()
|
||||
o_mesh.free()
|
||||
return volume
|
||||
|
||||
def get_gross_volume(self, o):
|
||||
def get_gross_volume(self, o: bpy.types.Object) -> float:
|
||||
if not self.has_openings(o):
|
||||
return self.get_net_volume(o)
|
||||
|
||||
@@ -529,16 +538,18 @@ class QtoCalculator:
|
||||
|
||||
return gross_volume
|
||||
|
||||
def has_openings(self, obj):
|
||||
def has_openings(
|
||||
self, obj: bpy.types.Object
|
||||
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return element and getattr(element, "HasOpenings", [])
|
||||
|
||||
def get_obj_decompositions(self, obj):
|
||||
def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
decompositions = ifcopenshell.util.element.get_decomposition(element)
|
||||
return decompositions
|
||||
|
||||
def get_gross_weight(self, obj):
|
||||
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -546,7 +557,7 @@ class QtoCalculator:
|
||||
gross_weight = obj_mass_density * gross_volume
|
||||
return gross_weight
|
||||
|
||||
def get_net_weight(self, obj):
|
||||
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
obj_mass_density = self.get_obj_mass_density(obj)
|
||||
if not obj_mass_density:
|
||||
return
|
||||
@@ -554,7 +565,7 @@ class QtoCalculator:
|
||||
net_weight = obj_mass_density * net_volume
|
||||
return net_weight
|
||||
|
||||
def get_obj_mass_density(self, obj):
|
||||
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
material = ifcopenshell.util.element.get_material(entity)
|
||||
if material is None:
|
||||
@@ -629,7 +640,7 @@ class QtoCalculator:
|
||||
# volume += v1.dot(v2.cross(v3)) / 6.0
|
||||
# return volume
|
||||
|
||||
def get_opening_type(self, opening, obj):
|
||||
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
|
||||
"""_summary_: Returns the opening type - OPENING / RECESS
|
||||
|
||||
:param blender-object opening: blender opening object
|
||||
@@ -649,14 +660,22 @@ class QtoCalculator:
|
||||
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
|
||||
|
||||
def get_opening_area(
|
||||
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
|
||||
):
|
||||
self,
|
||||
obj: bpy.types.Object,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
min_area: int = 0,
|
||||
ignore_recesses: bool = False,
|
||||
) -> float:
|
||||
"""_summary_: Returns the lateral area of the openings in the object.
|
||||
|
||||
:param obj: blender object
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
|
||||
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector lower than this value will be ignored, defaults to 45
|
||||
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
|
||||
Openings with a normal_vector greater than this value will be ignored,defaults to 135
|
||||
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
|
||||
defaults to 0
|
||||
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
|
||||
:return float: Opening Area
|
||||
"""
|
||||
@@ -702,14 +721,14 @@ class QtoCalculator:
|
||||
|
||||
def get_lateral_area(
|
||||
self,
|
||||
obj,
|
||||
obj: bpy.types.Object,
|
||||
subtract_openings: bool = True,
|
||||
exclude_end_areas: bool = False,
|
||||
exclude_side_areas: bool = False,
|
||||
angle_z1: int = 45,
|
||||
angle_z2: int = 135,
|
||||
main_axis: str = "",
|
||||
):
|
||||
) -> float:
|
||||
"""_summary_
|
||||
|
||||
:param blender-object obj: blender object, bpy.types.Object
|
||||
@@ -760,7 +779,7 @@ class QtoCalculator:
|
||||
area += polygon.area
|
||||
return area + total_opening_area
|
||||
|
||||
def get_gross_side_area(self, obj):
|
||||
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
|
||||
if not self.has_openings(obj):
|
||||
return self.get_net_side_area(obj)
|
||||
|
||||
@@ -768,15 +787,15 @@ class QtoCalculator:
|
||||
|
||||
return gross_side_area
|
||||
|
||||
def get_net_side_area(self, obj):
|
||||
def get_net_side_area(self, obj: bpy.types.Object) -> float:
|
||||
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
|
||||
return net_side_area
|
||||
|
||||
def get_outer_surface_area(self, obj):
|
||||
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
|
||||
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
|
||||
return outer_surface_area
|
||||
|
||||
def get_end_area(self, obj):
|
||||
def get_end_area(self, obj: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
gross_mesh = self.get_gross_element_mesh(element)
|
||||
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
|
||||
@@ -790,7 +809,7 @@ class QtoCalculator:
|
||||
|
||||
return end_area
|
||||
|
||||
def get_gross_top_area(self, obj, angle: int = 45):
|
||||
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
|
||||
"""_summary_: Returns the gross top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
@@ -827,12 +846,14 @@ class QtoCalculator:
|
||||
return area + opening_area
|
||||
|
||||
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
|
||||
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
|
||||
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
|
||||
"""_summary_: Returns the net top area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
|
||||
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
|
||||
Values lower than this will be ignored, defaults to 45
|
||||
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
|
||||
defaults to True
|
||||
:return float: Net Top Area
|
||||
"""
|
||||
z_axis = (0, 0, 1)
|
||||
@@ -852,11 +873,11 @@ class QtoCalculator:
|
||||
|
||||
return area
|
||||
|
||||
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
|
||||
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
|
||||
"""_summary_: Returns the projected area of the object.
|
||||
|
||||
:param blender-object obj: blender object
|
||||
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
|
||||
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
|
||||
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
|
||||
:return float: Projected Area
|
||||
"""
|
||||
@@ -891,7 +912,7 @@ class QtoCalculator:
|
||||
return projected_polygon.area + void_area
|
||||
return projected_polygon.area
|
||||
|
||||
def get_OBB_object(self, obj):
|
||||
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -932,7 +953,7 @@ class QtoCalculator:
|
||||
|
||||
return new_OBB_object
|
||||
|
||||
def get_AABB_object(self, obj):
|
||||
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -988,12 +1009,12 @@ class QtoCalculator:
|
||||
|
||||
def get_bisected_obj(
|
||||
self,
|
||||
obj,
|
||||
plane_co_pos,
|
||||
plane_no_pos,
|
||||
plane_co_neg,
|
||||
plane_no_neg,
|
||||
):
|
||||
obj: bpy.types.Object,
|
||||
plane_co_pos: VectorTuple,
|
||||
plane_no_pos: VectorTuple,
|
||||
plane_co_neg: VectorTuple,
|
||||
plane_no_neg: VectorTuple,
|
||||
) -> bpy.types.Object:
|
||||
"""_summary_: Returns the object bisected by two planes.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1031,11 +1052,12 @@ class QtoCalculator:
|
||||
|
||||
return bis_obj
|
||||
|
||||
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
|
||||
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
|
||||
"""_summary_: Returns the total contact area of the object with other objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return float: Total contact area of the object with other objects.
|
||||
"""
|
||||
total_contact_area = 0
|
||||
@@ -1046,11 +1068,12 @@ class QtoCalculator:
|
||||
|
||||
return total_contact_area
|
||||
|
||||
def get_touching_objects(self, obj, class_filter):
|
||||
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
|
||||
"""_summary_: Returns a list of objects that are touching the object.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:param list [] class_filter: A list of classes used to filter the objects
|
||||
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
|
||||
:return list: List of touching objects
|
||||
"""
|
||||
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
|
||||
@@ -1094,7 +1117,7 @@ class QtoCalculator:
|
||||
|
||||
return touching_objects
|
||||
|
||||
def get_contact_area(self, object1, object2):
|
||||
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the contact area between two objects.
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1109,7 +1132,13 @@ class QtoCalculator:
|
||||
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
|
||||
return total_area
|
||||
|
||||
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
|
||||
def get_intersection_between_polygons(
|
||||
self,
|
||||
object1: bpy.types.Object,
|
||||
poly1: bpy.types.MeshPolygon,
|
||||
object2: bpy.types.Object,
|
||||
poly2: bpy.types.MeshPolygon,
|
||||
) -> float:
|
||||
"""_summary_: Returns the intersection between two polygons.
|
||||
|
||||
:param blender-object object1: Blender Object
|
||||
@@ -1152,7 +1181,9 @@ class QtoCalculator:
|
||||
# TopologicalError - Generated Geometry might be invalid
|
||||
return 0
|
||||
|
||||
def create_shapely_polygon(self, obj, polygon, trans_matrix):
|
||||
def create_shapely_polygon(
|
||||
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
|
||||
) -> Polygon:
|
||||
"""_summary_: Create a shapely polygon
|
||||
|
||||
:param blender-object obj: Blender Object
|
||||
@@ -1171,12 +1202,14 @@ class QtoCalculator:
|
||||
polygon_tuples.append((x, y))
|
||||
return Polygon(polygon_tuples)
|
||||
|
||||
def get_gross_element_mesh(self, element):
|
||||
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
|
||||
return self.create_mesh_from_shape(element, settings)
|
||||
|
||||
def create_mesh_from_shape(self, element, settings=None):
|
||||
def create_mesh_from_shape(
|
||||
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
|
||||
) -> bpy.types.Mesh:
|
||||
if settings is None:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
@@ -1203,12 +1236,12 @@ class QtoCalculator:
|
||||
mesh.update()
|
||||
return mesh
|
||||
|
||||
def get_bmesh_from_mesh(self, mesh):
|
||||
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
return bm
|
||||
|
||||
def get_object_main_axis(self, o):
|
||||
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
|
||||
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
|
||||
|
||||
:param blender-object o: Blender Object
|
||||
@@ -1227,18 +1260,18 @@ class QtoCalculator:
|
||||
else:
|
||||
return "x"
|
||||
|
||||
def is_opening_horizontal(self, o):
|
||||
def is_opening_horizontal(self, o: bpy.types.Object) -> bool:
|
||||
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
|
||||
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
|
||||
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
|
||||
|
||||
return z < x and z < y
|
||||
|
||||
def delete_mesh(self, mesh):
|
||||
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
|
||||
mesh.user_clear()
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
def delete_obj(self, obj):
|
||||
def delete_obj(self, obj: bpy.types.Object) -> None:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ def get_active_pset_obj_name(context, obj_type):
|
||||
return ""
|
||||
|
||||
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True):
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
if "is_expanded" not in pset:
|
||||
@@ -114,16 +114,20 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
op = row.operator("bim.remove_pset", icon="X", text="")
|
||||
remove_pset_row = row.row(align=True)
|
||||
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
remove_pset_row.enabled = allow_removing
|
||||
elif props.active_pset_id != pset_id:
|
||||
row.label(text=pset["Name"], icon="COPY_ID")
|
||||
op = row.operator("bim.remove_pset", icon="X", text="")
|
||||
remove_pset_row = row.row(align=True)
|
||||
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
|
||||
op.pset_id = pset_id
|
||||
op.obj = obj_name
|
||||
op.obj_type = obj_type
|
||||
remove_pset_row.enabled = allow_removing
|
||||
if pset["is_expanded"]:
|
||||
if props.active_pset_id == pset_id:
|
||||
for prop in props.properties:
|
||||
@@ -171,7 +175,7 @@ def draw_psetqto_editable_ui(box, props, prop):
|
||||
|
||||
|
||||
class BIM_PT_object_psets(Panel):
|
||||
bl_label = "Object Property Sets"
|
||||
bl_label = "Property Sets"
|
||||
bl_idname = "BIM_PT_object_psets"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
@@ -203,17 +207,19 @@ class BIM_PT_object_psets(Panel):
|
||||
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
|
||||
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
|
||||
|
||||
for pset in ObjectPsetsData.data["psets"]:
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
|
||||
if ObjectPsetsData.data["psets"]:
|
||||
self.layout.label(text="Instance:")
|
||||
for pset in ObjectPsetsData.data["psets"]:
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
|
||||
|
||||
if ObjectPsetsData.data["inherited_psets"]:
|
||||
self.layout.label(text="Inherited Psets:", icon="FILE_PARENT")
|
||||
self.layout.label(text="Type:")
|
||||
for pset in ObjectPsetsData.data["inherited_psets"]:
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
|
||||
|
||||
|
||||
class BIM_PT_object_qtos(Panel):
|
||||
bl_label = "Object Quantity Sets"
|
||||
bl_label = "Quantity Sets"
|
||||
bl_idname = "BIM_PT_object_qtos"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
@@ -245,13 +251,15 @@ class BIM_PT_object_qtos(Panel):
|
||||
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO":
|
||||
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
|
||||
|
||||
for qto in ObjectQtosData.data["qtos"]:
|
||||
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
|
||||
if ObjectQtosData.data["qtos"]:
|
||||
self.layout.label(text="Instance:")
|
||||
for qto in ObjectQtosData.data["qtos"]:
|
||||
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
|
||||
|
||||
if ObjectQtosData.data["inherited_qsets"]:
|
||||
self.layout.label(text="Inherited Qsets:", icon="FILE_PARENT")
|
||||
self.layout.label(text="Type:")
|
||||
for qset in ObjectQtosData.data["inherited_qsets"]:
|
||||
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object")
|
||||
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
|
||||
|
||||
|
||||
class BIM_PT_material_psets(Panel):
|
||||
|
||||
@@ -34,6 +34,7 @@ classes = (
|
||||
operator.RemovePropTemplate,
|
||||
operator.RemovePsetTemplate,
|
||||
operator.RemovePsetTemplateFile,
|
||||
operator.SavePsetTemplateFile,
|
||||
prop.PsetTemplate,
|
||||
prop.EnumerationValues,
|
||||
prop.PropTemplate,
|
||||
|
||||
@@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
bl_idname = "bim.unlink_object"
|
||||
bl_label = "Unlink Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(name="Object Name")
|
||||
should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -227,6 +227,7 @@ class UnlinkObject(bpy.types.Operator):
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
|
||||
objects: list[bpy.types.Object]
|
||||
for obj in objects:
|
||||
was_active_object = obj == context.active_object
|
||||
|
||||
@@ -242,6 +243,27 @@ class UnlinkObject(bpy.types.Operator):
|
||||
if obj.data:
|
||||
obj_copy.data = obj.data.copy()
|
||||
|
||||
# prevent unlinking materials that might be used elsewhere
|
||||
replacements: dict[bpy.types.Material, bpy.types.Material] = dict()
|
||||
for material_slot in obj_copy.material_slots:
|
||||
material = material_slot.material
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
if material in replacements:
|
||||
material_replacement = replacements[material]
|
||||
|
||||
# no need to copy non-ifc materials as unlinking won't do anything to them
|
||||
elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None:
|
||||
replacements[material] = material
|
||||
continue
|
||||
|
||||
else:
|
||||
material_replacement = material.copy()
|
||||
replacements[material] = material_replacement
|
||||
|
||||
material_slot.material = material_replacement
|
||||
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
|
||||
obj = obj_copy
|
||||
|
||||
@@ -24,21 +24,15 @@ classes = (
|
||||
operator.ActivateIfcClassFilter,
|
||||
operator.AddFilter,
|
||||
operator.AddFilterGroup,
|
||||
operator.AddToIfcGroup,
|
||||
operator.ColourByProperty,
|
||||
operator.EditFilterQuery,
|
||||
operator.FilterModelElements,
|
||||
operator.IfcSelector,
|
||||
operator.LoadColourscheme,
|
||||
operator.LoadQuery,
|
||||
operator.LoadSearch,
|
||||
operator.OpenQueryLibrary,
|
||||
operator.RemoveFilter,
|
||||
operator.RemoveFilterGroup,
|
||||
operator.ResetObjectColours,
|
||||
operator.SaveColourscheme,
|
||||
operator.SaveSearch,
|
||||
operator.SaveSelectorQuery,
|
||||
operator.Search,
|
||||
operator.SelectByProperty,
|
||||
operator.SelectFilterElements,
|
||||
@@ -51,11 +45,6 @@ classes = (
|
||||
prop.BIMFilterClasses,
|
||||
prop.BIMFilterBuildingStoreys,
|
||||
prop.BIMSearchProperties,
|
||||
prop.SearchCollection,
|
||||
prop.SearchQueryFilter,
|
||||
prop.SearchQuery,
|
||||
prop.SearchQueryGroup,
|
||||
prop.IfcSelectorProperties,
|
||||
ui.BIM_PT_search,
|
||||
ui.BIM_PT_filter,
|
||||
ui.BIM_PT_colour_by_property,
|
||||
@@ -63,15 +52,12 @@ classes = (
|
||||
ui.BIM_UL_colourscheme,
|
||||
ui.BIM_UL_ifc_class_filter,
|
||||
ui.BIM_UL_ifc_building_storey_filter,
|
||||
ui.BIM_PT_IFCSelector,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
|
||||
bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMSearchProperties
|
||||
del bpy.types.Scene.IfcSelectorProperties
|
||||
|
||||
@@ -271,25 +271,29 @@ class ColourByProperty(Operator):
|
||||
colourscheme = {}
|
||||
|
||||
if len(props.colourscheme):
|
||||
colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
|
||||
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
|
||||
|
||||
for obj in context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
value = str(ifcopenshell.util.selector.get_element_value(element, query))
|
||||
if value not in colourscheme:
|
||||
colourscheme[value] = next(colours)[0:3]
|
||||
obj.color = (*colourscheme[value], 1)
|
||||
if value in colourscheme:
|
||||
colourscheme[value]["total"] += 1
|
||||
else:
|
||||
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
|
||||
obj.color = (*colourscheme[value]["colour"], 1)
|
||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
||||
if areas:
|
||||
areas[0].spaces[0].shading.color_type = "OBJECT"
|
||||
|
||||
props.colourscheme.clear()
|
||||
for value, colour in colourscheme.items():
|
||||
for value in sorted(colourscheme.keys()):
|
||||
data = colourscheme[value]
|
||||
new = props.colourscheme.add()
|
||||
new.name = str(value)
|
||||
new.colour = colour[0:3]
|
||||
new.total = data["total"]
|
||||
new.colour = data["colour"][0:3]
|
||||
return {"FINISHED"}
|
||||
|
||||
def store_state(self, context):
|
||||
@@ -351,16 +355,16 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
|
||||
query = props.colourscheme_query
|
||||
|
||||
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
|
||||
coulour_scheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
|
||||
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": cs.total} for cs in props.colourscheme}
|
||||
if group:
|
||||
group = group[0]
|
||||
description = json.loads(group.Description)
|
||||
description["colourscheme"] = coulour_scheme
|
||||
description["colourscheme"] = colourscheme
|
||||
description["colourscheme_query"] = query
|
||||
group.Description = json.dumps(description)
|
||||
else:
|
||||
description = json.dumps(
|
||||
{"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query}
|
||||
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
|
||||
)
|
||||
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
|
||||
|
||||
@@ -380,10 +384,11 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
|
||||
description = json.loads(group.Description)
|
||||
props.colourscheme_query = description.get("colourscheme_query")
|
||||
props.colourscheme.clear()
|
||||
for name, colour in description.get("colourscheme", {}).items():
|
||||
for name, data in description.get("colourscheme", {}).items():
|
||||
new = props.colourscheme.add()
|
||||
new.name = name
|
||||
new.colour = colour
|
||||
new.total = data["total"]
|
||||
new.colour = data["colour"]
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMSearchProperties
|
||||
@@ -623,223 +628,6 @@ class ShowAllElements(Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FilterModelElements(Operator):
|
||||
"""Filter model elements based on selection"""
|
||||
|
||||
bl_idname = "bim.filter_model_elements"
|
||||
bl_label = "Filter Model Elements"
|
||||
option: StringProperty("select|isolate|hide")
|
||||
|
||||
def execute(self, context):
|
||||
selection_props = context.scene.IfcSelectorProperties
|
||||
selection = (
|
||||
selection_props.selector_query_syntax
|
||||
if selection_props.manual_override
|
||||
else self.add_groups(selection_props)
|
||||
)
|
||||
selection_props.selector_query_syntax = selection
|
||||
r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
def add_groups(self, selector: str) -> str:
|
||||
selection = ""
|
||||
for group_index, group in enumerate(selector.groups):
|
||||
if group_index != 0:
|
||||
selection += " | "
|
||||
selection += "(" if len(selector.groups) > 1 else ""
|
||||
selection = self.add_queries(selection, group)
|
||||
|
||||
selection += ")" if len(selector.groups) > 1 else ""
|
||||
return selection
|
||||
|
||||
def add_queries(self, selection: str, group: str) -> str:
|
||||
for query_index, query in enumerate(group.queries):
|
||||
if query_index != 0:
|
||||
selection += " & " if query.and_or == "and" else " | "
|
||||
|
||||
if query.selector == "IFC Class":
|
||||
active_option = query.active_option.split(": ")[1]
|
||||
selection += f".{active_option}"
|
||||
selection = self.add_filters(selection, query)
|
||||
|
||||
elif query.selector == "GlobalId":
|
||||
selection += f"#{query.value}"
|
||||
|
||||
elif query.selector == "IfcElementType":
|
||||
index = int(query.active_sub_option.split(":")[0])
|
||||
selection += f"* #{query.sub_options[index].global_id}"
|
||||
|
||||
elif query.selector == "IfcSpatialElement":
|
||||
index = int(query.active_sub_option.split(":")[0])
|
||||
selection += f"@ #{query.sub_options[index].global_id}"
|
||||
return selection
|
||||
|
||||
def add_filters(self, selection: str, query: str) -> str:
|
||||
for f_index, f in enumerate(query.filters):
|
||||
if f_index != 0:
|
||||
selection += " & " if f.and_or == "and" else " | "
|
||||
selection += f".{query.active_option}"
|
||||
|
||||
if f.value in ("True", "False"):
|
||||
value = f.value
|
||||
elif f.value.isnumeric() and str(float(f.value))[0] == f.value[0]:
|
||||
value = f.value
|
||||
else:
|
||||
value = f'"{f.value}"'
|
||||
|
||||
selection += "["
|
||||
|
||||
if f.selector == "IfcPropertySet":
|
||||
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}'
|
||||
elif f.selector == "Attribute":
|
||||
# we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej
|
||||
pattern = re.compile(r"^[0-9]+:")
|
||||
match = pattern.search(f.active_option)
|
||||
selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}'
|
||||
selection += "]"
|
||||
return selection
|
||||
|
||||
|
||||
# This needs to be moved into ui code, I know ;) - vulevukusej
|
||||
class IfcSelector(Operator):
|
||||
"""Select elements in model with IFC Selector"""
|
||||
|
||||
bl_idname = "bim.ifc_selector"
|
||||
bl_label = "Select elements with IFC Selector"
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file()
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
from . import ui
|
||||
|
||||
ui.IfcSelectorUI.draw(context, self.layout)
|
||||
|
||||
|
||||
class SaveSelectorQuery(Operator):
|
||||
bl_idname = "bim.save_selector_query"
|
||||
bl_label = "Save Selector Query"
|
||||
save_name: StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "save_name")
|
||||
|
||||
def execute(self, context):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
new = ifc_selector.query_library.add()
|
||||
new.name = self.save_name
|
||||
new.query = ifc_selector.selector_query_syntax
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenQueryLibrary(Operator):
|
||||
"""Open Query Library"""
|
||||
|
||||
bl_idname = "bim.open_query_library"
|
||||
bl_label = "Open Query Library"
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
|
||||
for index, query in enumerate(ifc_selector.query_library):
|
||||
row = layout.row(align=True)
|
||||
row.prop(query, "query", text=query.name)
|
||||
op = row.operator("bim.load_query", icon="SORT_ASC", text="")
|
||||
op.index = index
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "query_library"
|
||||
op.index = index
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LoadQuery(Operator):
|
||||
bl_idname = "bim.load_query"
|
||||
bl_label = "Load Query"
|
||||
index: IntProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
close_operator_panel(event)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddToIfcGroup(Operator):
|
||||
bl_idname = "bim.add_to_ifc_group"
|
||||
bl_label = "Add to IFC Group"
|
||||
group_name: StringProperty(name="Group Name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
bpy.ops.bim.load_groups()
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMGroupProperties
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_group")
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_groups",
|
||||
"",
|
||||
self.props,
|
||||
"groups",
|
||||
self.props,
|
||||
"active_group_index",
|
||||
)
|
||||
|
||||
if self.props.active_group_id:
|
||||
for attribute in self.props.group_attributes:
|
||||
if attribute.name in ["Name", "Description"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(attribute, "string_value", text=attribute.name)
|
||||
|
||||
def execute(self, context):
|
||||
active_group_index = self.props.active_group_index
|
||||
ifc_definition_id = self.props.groups[active_group_index].ifc_definition_id
|
||||
|
||||
bpy.ops.bim.enable_editing_group(group=ifc_definition_id)
|
||||
|
||||
selector_query_syntax = context.scene.IfcSelectorProperties.selector_query_syntax
|
||||
|
||||
for attribute in self.props.group_attributes:
|
||||
if attribute.name == "Description":
|
||||
if "*selector*" not in attribute.string_value:
|
||||
attribute.string_value += f" *selector*{selector_query_syntax}*selector*"
|
||||
else:
|
||||
new_description = attribute.string_value.split("*selector*")
|
||||
new_description[1] = selector_query_syntax
|
||||
attribute.string_value = "*selector*".join(new_description)
|
||||
|
||||
bpy.ops.bim.edit_group()
|
||||
bpy.ops.bim.disable_group_editing_ui()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectSimilar(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.select_similar"
|
||||
bl_label = "Select Similar"
|
||||
|
||||
@@ -78,7 +78,7 @@ def update_is_container_selected(self, context):
|
||||
else:
|
||||
for obj in context.selected_objects:
|
||||
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
|
||||
if (container and container.Name == self.name) or (not container and self.name== "None"):
|
||||
if (container and container.Name == self.name) or (not container and self.name == "None"):
|
||||
obj.select_set(False)
|
||||
new = self.unselected_objects.add()
|
||||
new.obj = obj
|
||||
@@ -100,6 +100,7 @@ class BIMFilterBuildingStoreys(PropertyGroup):
|
||||
|
||||
class BIMColour(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
total: IntProperty(name="Total")
|
||||
colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0)
|
||||
|
||||
|
||||
@@ -138,139 +139,3 @@ def get_classes(self, ifc_product):
|
||||
declarations = util.schema.get_subtypes(declaration)
|
||||
names = [d.name() for d in declarations]
|
||||
return [(c, c, "") for c in sorted(names)]
|
||||
|
||||
|
||||
def load_sub_options(self, context):
|
||||
if self.selector not in ["IfcClass"]:
|
||||
self.load_option = "sub_options"
|
||||
load_selection_options(self, context)
|
||||
|
||||
|
||||
def load_selection_options(self, context):
|
||||
ifc = IfcStore.file
|
||||
load_option = self.load_option
|
||||
op = getattr(self, load_option)
|
||||
op.clear()
|
||||
options = []
|
||||
|
||||
if load_option == "options":
|
||||
self.sub_options.clear()
|
||||
if self.selector == "IFC Class":
|
||||
options = get_classes(self, "IfcElement")
|
||||
options.append(("IfcSpace", "IfcSpace", ""))
|
||||
elif self.selector == "IfcSpatialElement":
|
||||
options = get_classes(self, "IfcSpatialElement")
|
||||
elif self.selector == "IfcElementType":
|
||||
options = get_classes(self, "IfcElementType")
|
||||
elif self.selector == "GlobalId":
|
||||
return
|
||||
elif self.selector == "IfcPropertySet":
|
||||
psets = Selector.parse(ifc, ".IfcPropertySet")
|
||||
options = set([o.Name for o in psets])
|
||||
elif self.selector == "Attribute":
|
||||
attributes = ["GlobalId", "Name", "Description", "ObjectType", "Tag", "PredefinedType"]
|
||||
for a in attributes:
|
||||
options.append((a, a, ""))
|
||||
|
||||
elif load_option == "sub_options":
|
||||
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
|
||||
active_option = self.active_option.split(": ")[1]
|
||||
options = Selector.parse(ifc, f".{active_option}")
|
||||
|
||||
elif self.selector == "IfcPropertySet":
|
||||
options = set()
|
||||
active_pset = self.active_option.split(": ")[1]
|
||||
psets_in_file = Selector.parse(ifc, f'.IfcPropertySet[Name="{active_pset}"]')
|
||||
for pset in psets_in_file:
|
||||
for prop in pset.HasProperties:
|
||||
options.add(prop.Name)
|
||||
|
||||
for index, option in enumerate(options):
|
||||
new = op.add()
|
||||
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
|
||||
if self.load_option == "sub_options":
|
||||
new.name = f"{index}: {option.Name}"
|
||||
new.global_id = option.GlobalId
|
||||
else:
|
||||
new.name = f"{index}: {option[0]}"
|
||||
elif self.selector == "IfcPropertySet":
|
||||
new.name = f"{index}: {option}"
|
||||
else:
|
||||
new.name = f"{index}: {option[0]}"
|
||||
self.load_option = "options"
|
||||
|
||||
|
||||
def load_spatial_elements(self, context):
|
||||
ifc = IfcStore.file
|
||||
col_items = Selector.parse(ifc, f".{self.selected_spatial_element}")
|
||||
collection = getattr(self, "sub_spatial_elements", None)
|
||||
collection.clear()
|
||||
for index, c in enumerate(col_items):
|
||||
new = collection.add()
|
||||
new.name = f"{index}-{c.Name}"
|
||||
|
||||
|
||||
class SearchCollection(PropertyGroup):
|
||||
name: StringProperty()
|
||||
long_name: StringProperty()
|
||||
global_id: StringProperty()
|
||||
query: StringProperty()
|
||||
|
||||
|
||||
class IfcSelector:
|
||||
and_or: EnumProperty(
|
||||
items=[(i, i, i) for i in ["and", "or"]],
|
||||
)
|
||||
negation: BoolProperty(name="not")
|
||||
comparison: EnumProperty(
|
||||
items=[
|
||||
("=", "equal to", ""),
|
||||
("*=", "contains", ""),
|
||||
(">=", "greater than or equal to", ""),
|
||||
("<=", "lesser than or equal to", ""),
|
||||
(">", "greater than", ""),
|
||||
("<", "less than", ""),
|
||||
],
|
||||
)
|
||||
load_option: StringProperty(
|
||||
default="options", description="controls whether or not options or sub_options are loaded"
|
||||
)
|
||||
options: CollectionProperty(type=SearchCollection)
|
||||
active_option: StringProperty(update=load_sub_options)
|
||||
sub_options: CollectionProperty(type=SearchCollection)
|
||||
active_sub_option: StringProperty()
|
||||
value: StringProperty(description="generic 'value' that can be used in multiple scenarios")
|
||||
|
||||
|
||||
class SearchQueryFilter(PropertyGroup, IfcSelector):
|
||||
selector: EnumProperty(
|
||||
items=[(i, i, i) for i in ["-", "IfcPropertySet", "Attribute"]],
|
||||
name="Filter selection by",
|
||||
update=load_selection_options,
|
||||
default="-",
|
||||
)
|
||||
|
||||
|
||||
class SearchQuery(PropertyGroup, IfcSelector):
|
||||
filters: CollectionProperty(type=SearchQueryFilter)
|
||||
selector: EnumProperty(
|
||||
items=[(i, i, i) for i in ["-", "IFC Class", "IfcSpatialElement", "IfcElementType", "GlobalId"]],
|
||||
name="Selector type",
|
||||
update=load_selection_options,
|
||||
default="-",
|
||||
)
|
||||
|
||||
|
||||
class SearchQueryGroup(PropertyGroup, IfcSelector):
|
||||
queries: CollectionProperty(type=SearchQuery)
|
||||
|
||||
|
||||
class IfcSelectorProperties(PropertyGroup, IfcSelector):
|
||||
groups: CollectionProperty(type=SearchQueryGroup)
|
||||
selector_query_syntax: StringProperty()
|
||||
|
||||
query_library: CollectionProperty(type=SearchCollection)
|
||||
active_query: StringProperty()
|
||||
|
||||
active_query: StringProperty()
|
||||
manual_override: BoolProperty(default=False, description="Toggle to allow manual typing of query-syntax")
|
||||
|
||||
@@ -125,7 +125,7 @@ class BIM_UL_colourscheme(bpy.types.UIList):
|
||||
if not item:
|
||||
return
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name)
|
||||
row.label(text=f"{item.name} ({item.total})")
|
||||
row.prop(item, "colour", text="")
|
||||
|
||||
|
||||
@@ -159,153 +159,3 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList):
|
||||
split = split.column()
|
||||
split.scale_x = 0.5
|
||||
split.label(text=str(item.total))
|
||||
|
||||
|
||||
class BIM_PT_IFCSelector(Panel):
|
||||
bl_label = "Selector"
|
||||
bl_idname = "BIM_PT_ifc_selector"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_sandbox"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("bim.ifc_selector")
|
||||
|
||||
|
||||
# this doesn't inherit from Panel since it's just a class to abstract away UI code from the IfcSelector Operator
|
||||
class IfcSelectorUI:
|
||||
@classmethod
|
||||
def draw(self, context, layout):
|
||||
ifc_selector = context.scene.IfcSelectorProperties
|
||||
row = layout.row()
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", text="Add selection group")
|
||||
op.option = "add"
|
||||
op.collection = "groups"
|
||||
layout.separator()
|
||||
|
||||
self.draw_query_group_ui(self, ifc_selector, layout)
|
||||
|
||||
if len(ifc_selector.groups) != 0:
|
||||
row = layout.row(align=True)
|
||||
row.prop(ifc_selector, "selector_query_syntax", text="Query Syntax")
|
||||
row.prop(ifc_selector, "manual_override", text="")
|
||||
|
||||
select = row.operator("bim.filter_model_elements", text="", icon="RESTRICT_SELECT_OFF")
|
||||
select.option = "select"
|
||||
isolate = row.operator("bim.filter_model_elements", text="", icon="ZOOM_SELECTED")
|
||||
isolate.option = "isolate"
|
||||
hide = row.operator("bim.filter_model_elements", text="", icon="HIDE_ON")
|
||||
hide.option = "hide"
|
||||
unhide = row.operator("bim.filter_model_elements", text="", icon="HIDE_OFF")
|
||||
unhide.option = "unhide"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator("bim.save_selector_query", text="Save Query")
|
||||
op = row.operator("bim.open_query_library", text="Load Query")
|
||||
row.operator("bim.add_to_ifc_group", text="Add to IFC Group")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.show_scene_elements", text="Show All Elements", icon="HIDE_OFF")
|
||||
|
||||
def draw_query_group_ui(self, ifc_selector, layout):
|
||||
for index, group in enumerate(ifc_selector.groups):
|
||||
row = layout.row()
|
||||
row.alignment = "CENTER"
|
||||
row.label(text="or") if index != 0 else None
|
||||
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.label(text=f"Group #{str(index+1)}")
|
||||
row.context_pointer_set(name="bim_prop_group", data=group)
|
||||
op = row.operator("bim.edit_blender_collection", text="Add query", icon="PLUS")
|
||||
op.option = "add"
|
||||
op.collection = "queries"
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
|
||||
op = row.operator("bim.edit_blender_collection", text="Remove selection group")
|
||||
op.option = "remove"
|
||||
op.collection = "groups"
|
||||
op.index = index
|
||||
|
||||
self.draw_query_ui(self, group, box)
|
||||
|
||||
def draw_query_ui(self, group, box):
|
||||
for index, query in enumerate(group.queries):
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
|
||||
row.prop(query, "and_or", text="") if index != 0 else None
|
||||
if query.and_or == "or":
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.prop(query, "selector", text="")
|
||||
|
||||
if query.selector in ["IFC Class", "IfcSpatialElement", "IfcElementType"]:
|
||||
row.label(text="Equals")
|
||||
row.prop_search(query, "active_option", query, "options", text="")
|
||||
row.prop_search(query, "active_sub_option", query, "sub_options", text="") if len(
|
||||
query.sub_options
|
||||
) != 0 else None
|
||||
self.draw_filter_ui(self, box, query)
|
||||
|
||||
elif query.selector in ["GlobalId", "Attribute"]:
|
||||
row.prop(query, "negation", text="")
|
||||
row.prop(query, "comparison", text="")
|
||||
row.prop(query, "value", text="")
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=group)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "queries"
|
||||
op.index = index
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=query)
|
||||
op = (
|
||||
row.operator("bim.edit_blender_collection", text="Add filter")
|
||||
if query.selector == "IFC Class"
|
||||
else None
|
||||
)
|
||||
if op:
|
||||
op.option = "add"
|
||||
op.collection = "filters"
|
||||
|
||||
def draw_filter_ui(self, box, query):
|
||||
for filter_index, f in enumerate(query.filters):
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.label(text=" ↪")
|
||||
row.prop(f, "and_or", text="") if filter_index != 0 else None
|
||||
if f.and_or == "or":
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.label(text=" ↪")
|
||||
row.prop(f, "selector", text="")
|
||||
|
||||
if f.selector == "Attribute":
|
||||
row.prop_search(f, "active_option", f, "options", text="", results_are_suggestions=True)
|
||||
row.prop(
|
||||
f,
|
||||
"negation",
|
||||
)
|
||||
row.prop(f, "comparison", text="")
|
||||
row.prop(f, "value", text="")
|
||||
|
||||
elif f.selector == "IfcPropertySet":
|
||||
row.prop_search(f, "active_option", f, "options", text="")
|
||||
row.prop_search(f, "active_sub_option", f, "sub_options", text="")
|
||||
row.prop(f, "negation")
|
||||
row.prop(f, "comparison", text="")
|
||||
row.prop(f, "value", text="")
|
||||
|
||||
row.context_pointer_set(name="bim_prop_group", data=query)
|
||||
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
|
||||
op.option = "remove"
|
||||
op.collection = "filters"
|
||||
op.index = filter_index
|
||||
|
||||
@@ -388,13 +388,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
|
||||
should_delete_existing_annotation = False
|
||||
should_create_annotation = False
|
||||
|
||||
# remove annotation only if the reference object was changed
|
||||
# otherwise we rely on the existing annotation
|
||||
if annotation:
|
||||
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
|
||||
should_delete_existing_annotation = True
|
||||
elif not reference_obj:
|
||||
should_delete_existing_annotation = True
|
||||
|
||||
if reference_obj and (should_delete_existing_annotation or not annotation):
|
||||
if (should_delete_existing_annotation or not annotation):
|
||||
should_create_annotation = True
|
||||
|
||||
if should_delete_existing_annotation:
|
||||
|
||||
@@ -76,7 +76,7 @@ class Blender:
|
||||
def get_name(cls, ifc_class, name): pass
|
||||
def get_selected_objects(cls): pass
|
||||
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass
|
||||
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): pass
|
||||
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass
|
||||
def is_ifc_object(cls, obj): pass
|
||||
def is_ifc_class_active(cls, ifc_class): pass
|
||||
def get_viewport_context(cls): pass
|
||||
@@ -153,6 +153,17 @@ class Bsdd:
|
||||
def should_load_preview_domains(cls): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Clash:
|
||||
def export_clash_sets(cls): pass
|
||||
def get_clash(cls, clash_set, a_global_id, b_global_id): pass
|
||||
def get_clash_set(cls, name): pass
|
||||
def get_clash_sets(cls): pass
|
||||
def import_active_clashes(cls): pass
|
||||
def load_clash_sets(cls, fn): pass
|
||||
def look_at(cls, target, location): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Collector:
|
||||
def assign(cls, obj): pass
|
||||
@@ -508,7 +519,7 @@ class Misc:
|
||||
class Model:
|
||||
def convert_si_to_unit(cls, value): pass
|
||||
def convert_unit_to_si(cls, value): pass
|
||||
def export_curve(cls, position, edge_indices, points=None): pass
|
||||
def export_curve(cls, position, edge_indices): pass
|
||||
def export_points(cls, position, indices): pass
|
||||
def export_profile(cls, obj, position=None): pass
|
||||
def generate_occurrence_name(cls, element_type, ifc_class): pass
|
||||
|
||||
@@ -22,6 +22,7 @@ from blenderbim.tool.boundary import Boundary
|
||||
from blenderbim.tool.brick import Brick
|
||||
from blenderbim.tool.bsdd import Bsdd
|
||||
from blenderbim.tool.cad import Cad
|
||||
from blenderbim.tool.clash import Clash
|
||||
from blenderbim.tool.collector import Collector
|
||||
from blenderbim.tool.context import Context
|
||||
from blenderbim.tool.debug import Debug
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import json
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim
|
||||
import addon_utils
|
||||
from mathutils import Vector
|
||||
from pathlib import Path
|
||||
import addon_utils
|
||||
from blenderbim.bim.ifc import IFC_CONNECTED_TYPE
|
||||
from typing import Any, Optional, Union, Literal, Iterable, Callable
|
||||
|
||||
|
||||
VIEWPORT_ATTRIBUTES = [
|
||||
@@ -37,13 +41,16 @@ VIEWPORT_ATTRIBUTES = [
|
||||
"clip_planes",
|
||||
]
|
||||
|
||||
OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera]
|
||||
|
||||
|
||||
class Blender(blenderbim.core.tool.Blender):
|
||||
OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE")
|
||||
OBJECT_TYPES_THAT_SUPPORT_EDIT_GPENCIL_MODE = ("GPENCIL",)
|
||||
TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME" if bpy.app.version >= (4, 1, 0) else "LIGHTPROBE_GRID"
|
||||
|
||||
@classmethod
|
||||
def get_area_props(cls, context):
|
||||
def get_area_props(cls, context: bpy.types.Context) -> Any:
|
||||
try:
|
||||
if context.screen.name.endswith("-nonnormal"): # Ctrl-space temporary fullscreen
|
||||
screen = bpy.data.screens[context.screen.name[0 : -len("-nonnormal")]]
|
||||
@@ -55,12 +62,12 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def set_active_object(cls, obj):
|
||||
def set_active_object(cls, obj: bpy.types.Object) -> None:
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def setup_tabs(cls):
|
||||
def setup_tabs(cls) -> None:
|
||||
# https://blender.stackexchange.com/questions/140644/how-can-make-the-state-of-a-boolean-property-relative-to-the-3d-view-area
|
||||
for screen in bpy.data.screens:
|
||||
if len(screen.BIMAreaProperties) == 20:
|
||||
@@ -70,7 +77,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
screen.BIMAreaProperties.add()
|
||||
|
||||
@classmethod
|
||||
def is_tab(cls, context, tab):
|
||||
def is_tab(cls, context: bpy.types.Context, tab: str) -> bool:
|
||||
aprops = cls.get_area_props(context)
|
||||
if not aprops:
|
||||
return context.screen.BIMTabProperties.tab == tab
|
||||
@@ -79,7 +86,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return aprops.tab == tab
|
||||
|
||||
@classmethod
|
||||
def is_default_scene(cls):
|
||||
def is_default_scene(cls) -> bool:
|
||||
if len(bpy.context.scene.objects) != 3:
|
||||
return False
|
||||
if {obj.type for obj in bpy.context.scene.objects} == {"MESH", "LIGHT", "CAMERA"}:
|
||||
@@ -87,7 +94,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_name(cls, ifc_class, name):
|
||||
def get_name(cls, ifc_class: str, name: str) -> str:
|
||||
if not bpy.data.objects.get(f"{ifc_class}/{name}"):
|
||||
return name
|
||||
i = 2
|
||||
@@ -96,13 +103,15 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return f"{name} {i}"
|
||||
|
||||
@classmethod
|
||||
def get_selected_objects(cls):
|
||||
def get_selected_objects(cls) -> set[bpy.types.Object]:
|
||||
if bpy.context.selected_objects:
|
||||
return set(bpy.context.selected_objects + [bpy.context.active_object])
|
||||
return set([bpy.context.active_object])
|
||||
|
||||
@classmethod
|
||||
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None) -> bpy.types.Object:
|
||||
def create_ifc_object(
|
||||
cls, ifc_class: str, name: Optional[str] = None, data: Optional[OBJECT_DATA_TYPE] = None
|
||||
) -> bpy.types.Object:
|
||||
name = name or "My " + ifc_class
|
||||
name = cls.get_name(ifc_class, name)
|
||||
obj = bpy.data.objects.new(name, data)
|
||||
@@ -110,7 +119,11 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None):
|
||||
def get_obj_ifc_definition_id(
|
||||
cls, obj: Optional[str] = None, obj_type: Optional[str] = None, context: Optional[bpy.types.Context] = None
|
||||
) -> Union[int, None]:
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if obj_type == "Object":
|
||||
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
|
||||
elif obj_type == "Material":
|
||||
@@ -122,41 +135,42 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
elif obj_type == "MaterialSetItem":
|
||||
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
|
||||
elif obj_type == "Task":
|
||||
return bpy.context.scene.BIMTaskTreeProperties.tasks[
|
||||
bpy.context.scene.BIMWorkScheduleProperties.active_task_index
|
||||
return context.scene.BIMTaskTreeProperties.tasks[
|
||||
context.scene.BIMWorkScheduleProperties.active_task_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Cost":
|
||||
return bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
return context.scene.BIMCostProperties.cost_items[
|
||||
context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Resource":
|
||||
return bpy.context.scene.BIMResourceTreeProperties.resources[
|
||||
bpy.context.scene.BIMResourceProperties.active_resource_index
|
||||
return context.scene.BIMResourceTreeProperties.resources[
|
||||
context.scene.BIMResourceProperties.active_resource_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "Profile":
|
||||
return bpy.context.scene.BIMProfileProperties.profiles[
|
||||
bpy.context.scene.BIMProfileProperties.active_profile_index
|
||||
return context.scene.BIMProfileProperties.profiles[
|
||||
context.scene.BIMProfileProperties.active_profile_index
|
||||
].ifc_definition_id
|
||||
elif obj_type == "WorkSchedule":
|
||||
return bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id
|
||||
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
|
||||
elif obj_type == "Group":
|
||||
prop = bpy.context.scene.BIMGroupProperties
|
||||
prop = context.scene.BIMGroupProperties
|
||||
return prop.groups[prop.active_group_index].ifc_definition_id
|
||||
|
||||
@classmethod
|
||||
def is_ifc_object(cls, obj):
|
||||
def is_ifc_object(cls, obj: bpy.types.Object) -> bool:
|
||||
return bool(obj.BIMObjectProperties.ifc_definition_id)
|
||||
|
||||
@classmethod
|
||||
def is_ifc_class_active(cls, ifc_class):
|
||||
if bpy.context.active_object:
|
||||
if cls.is_ifc_object(bpy.context.active_object):
|
||||
return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class)
|
||||
def is_ifc_class_active(cls, ifc_class: str) -> bool:
|
||||
obj = bpy.context.active_object
|
||||
if obj:
|
||||
if cls.is_ifc_object(obj):
|
||||
return tool.Ifc.get_entity(obj).is_a(ifc_class)
|
||||
return False
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def show_info_message(cls, text, message_type="INFO"):
|
||||
def show_info_message(cls, text: str, message_type: Literal["INFO", "ERROR"] = "INFO") -> None:
|
||||
"""useful for showing error messages outside blender operators
|
||||
|
||||
Possible `message_type`: `INFO` / `ERROR`"""
|
||||
@@ -167,14 +181,14 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type)
|
||||
|
||||
@classmethod
|
||||
def get_view3d_area(cls):
|
||||
def get_view3d_area(cls) -> Union[bpy.types.Area, None]:
|
||||
for window in bpy.context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
return area
|
||||
|
||||
@classmethod
|
||||
def get_blender_prop_default_value(cls, props, prop_name):
|
||||
def get_blender_prop_default_value(cls, props, prop_name: str) -> Any:
|
||||
prop_bl_rna = props.bl_rna.properties[prop_name]
|
||||
if getattr(prop_bl_rna, "array_length", 0) > 0:
|
||||
prop_value = prop_bl_rna.default_array
|
||||
@@ -183,7 +197,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return prop_value
|
||||
|
||||
@classmethod
|
||||
def get_viewport_context(cls):
|
||||
def get_viewport_context(cls) -> dict:
|
||||
"""Get viewport area context for context overriding.
|
||||
|
||||
Useful for calling operators outside viewport context.
|
||||
@@ -198,25 +212,25 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return context_override
|
||||
|
||||
@classmethod
|
||||
def get_viewport_position(cls):
|
||||
def get_viewport_position(cls) -> dict:
|
||||
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
|
||||
copy_if_possible = lambda x: x.copy() if hasattr(x, "copy") else x
|
||||
viewport_data = {attr: copy_if_possible(getattr(region_3d, attr)) for attr in VIEWPORT_ATTRIBUTES}
|
||||
return viewport_data
|
||||
|
||||
@classmethod
|
||||
def set_viewport_position(cls, data):
|
||||
def set_viewport_position(cls, data: dict) -> None:
|
||||
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
|
||||
for attr in VIEWPORT_ATTRIBUTES:
|
||||
setattr(region_3d, attr, data[attr])
|
||||
|
||||
@classmethod
|
||||
def set_viewport_tool(cls, tool_name):
|
||||
def set_viewport_tool(cls, tool_name: str) -> None:
|
||||
with bpy.context.temp_override(**tool.Blender.get_viewport_context()):
|
||||
bpy.ops.wm.tool_set_by_id(name=tool_name)
|
||||
|
||||
@classmethod
|
||||
def get_shader_editor_context(cls):
|
||||
def get_shader_editor_context(cls) -> Union[dict, None]:
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == "NODE_EDITOR":
|
||||
@@ -226,7 +240,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return context_override
|
||||
|
||||
@classmethod
|
||||
def copy_node_graph(cls, material_to, material_from):
|
||||
def copy_node_graph(cls, material_to: bpy.types.Material, material_from: bpy.types.Material) -> None:
|
||||
# https://projects.blender.org/blender/blender/issues/108763
|
||||
if bpy.app.version[:2] == (4, 0):
|
||||
print(
|
||||
@@ -270,7 +284,9 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
shader_editor.pin = previous_pin_setting
|
||||
|
||||
@classmethod
|
||||
def get_material_node(cls, blender_material, node_type, kwargs={}):
|
||||
def get_material_node(
|
||||
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
|
||||
) -> Union[bpy.types.ShaderNode, None]:
|
||||
"""returns first node from the `blender_material` shader graph with type `node_type`"""
|
||||
if not blender_material.use_nodes:
|
||||
return
|
||||
@@ -280,15 +296,15 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return node
|
||||
|
||||
@classmethod
|
||||
def update_screen(cls):
|
||||
def update_screen(cls) -> None:
|
||||
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
|
||||
|
||||
@classmethod
|
||||
def update_viewport(cls):
|
||||
def update_viewport(cls) -> None:
|
||||
tool.Blender.get_viewport_context()["area"].tag_redraw()
|
||||
|
||||
@classmethod
|
||||
def force_depsgraph_update(cls):
|
||||
def force_depsgraph_update(cls) -> None:
|
||||
"""useful if you need to trigger callbacks like `depsgraph_update_pre`"""
|
||||
# blender is requiring some ID to be changed
|
||||
# to trigger depsgraph update
|
||||
@@ -297,9 +313,11 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
@classmethod
|
||||
def ensure_unique_name(cls, name, objects, iteration=0):
|
||||
def ensure_unique_name(cls, name: str, objects: Iterable[str], iteration=0) -> str:
|
||||
"""returns a unique name for the given name and dictionary of objects
|
||||
blender style name with .001, .002, etc. suffix
|
||||
blender style name with .001, .002, etc. suffix.
|
||||
|
||||
objects can be `bpy.data.objects`.
|
||||
"""
|
||||
current_iteration = name if not iteration else f"{name}.{iteration:03d}"
|
||||
if current_iteration not in objects:
|
||||
@@ -307,7 +325,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return cls.ensure_unique_name(name, objects, iteration + 1)
|
||||
|
||||
@classmethod
|
||||
def blender_path_to_posix(cls, blender_path):
|
||||
def blender_path_to_posix(cls, blender_path: str) -> str:
|
||||
"""Process blender path to be saved as posix.
|
||||
|
||||
If path is relative the method will keep it relative to .ifc file
|
||||
@@ -322,7 +340,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return path.as_posix()
|
||||
|
||||
@classmethod
|
||||
def get_default_selection_keypmap(cls):
|
||||
def get_default_selection_keypmap(cls) -> tuple:
|
||||
"""keymap to replicate default blender selection behaviour with click and box selection"""
|
||||
# code below comes from blender_default.py which is part of default blender scripts licensed under GPL v2
|
||||
# https://github.com/blender/blender/blob/master/release/scripts/presets/keyconfig/keymap_data/blender_default.py
|
||||
@@ -369,7 +387,9 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return keymap
|
||||
|
||||
@classmethod
|
||||
def add_layout_hotkey_operator(cls, tool_name, layout, text, hotkey, description):
|
||||
def add_layout_hotkey_operator(
|
||||
cls, tool_name: str, layout: bpy.types.UILayout, text: str, hotkey: str, description: str
|
||||
) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]:
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"S": "EVENT_SHIFT",
|
||||
@@ -386,7 +406,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return op, row
|
||||
|
||||
@classmethod
|
||||
def get_object_bounding_box(cls, obj):
|
||||
def get_object_bounding_box(cls, obj: bpy.types.Object) -> dict:
|
||||
"""Returns dict with local min and max x, y, z values for the object.
|
||||
|
||||
Careful with using this method for objects in EDIT mode because
|
||||
@@ -407,14 +427,20 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return bbox_dict
|
||||
|
||||
@classmethod
|
||||
def select_and_activate_single_object(cls, context, active_object):
|
||||
def select_and_activate_single_object(cls, context: bpy.types.Context, active_object: bpy.types.Object) -> None:
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
context.view_layer.objects.active = active_object
|
||||
active_object.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def set_objects_selection(cls, context, active_object, selected_objects, clear_previous_selection=True):
|
||||
def set_objects_selection(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
active_object: Optional[bpy.types.Object] = None,
|
||||
selected_objects: list[bpy.types.Object] = list(),
|
||||
clear_previous_selection=True,
|
||||
) -> None:
|
||||
if clear_previous_selection:
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
@@ -425,7 +451,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
active_object.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def enum_property_has_valid_index(cls, props, prop_name, enum_items):
|
||||
def enum_property_has_valid_index(cls, props, prop_name: str, enum_items: tuple) -> bool:
|
||||
"""method created for readibility and to avoid console warnings like
|
||||
`pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'`
|
||||
"""
|
||||
@@ -436,7 +462,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return current_value_index < len(enum_items)
|
||||
|
||||
@classmethod
|
||||
def append_data_block(cls, filepath, data_block_type, name, link=False, relative=False):
|
||||
def append_data_block(cls, filepath: str, data_block_type: str, name: str, link=False, relative=False) -> dict:
|
||||
if Path(filepath) == Path(bpy.data.filepath):
|
||||
data_block = getattr(bpy.data, data_block_type).get(name, None)
|
||||
if not data_block:
|
||||
@@ -450,7 +476,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""}
|
||||
|
||||
@classmethod
|
||||
def remove_data_block(cls, data_block, do_unlink=True):
|
||||
def remove_data_block(cls, data_block: bpy.types.ID, do_unlink=True) -> None:
|
||||
"""Removes a datablock (such as a mesh)
|
||||
|
||||
See https://projects.blender.org/blender/blender/issues/118787 for more
|
||||
@@ -472,7 +498,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
|
||||
## BMESH UTILS ##
|
||||
@classmethod
|
||||
def apply_bmesh(cls, mesh, bm, obj=None):
|
||||
def apply_bmesh(cls, mesh: bpy.types.Mesh, bm: bmesh.types.BMesh, obj: Optional[bpy.types.Object] = None) -> None:
|
||||
"""`obj` argument is not optional if you plan to update mesh in EDIT mode
|
||||
and it's possible that that mesh object won't be currenly active."""
|
||||
import bmesh
|
||||
@@ -506,7 +532,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
mesh.update()
|
||||
|
||||
@classmethod
|
||||
def get_bmesh_for_mesh(cls, mesh, clean=False):
|
||||
def get_bmesh_for_mesh(cls, mesh: bpy.types.Mesh, clean=False) -> bmesh.types.BMesh:
|
||||
import bmesh
|
||||
|
||||
if mesh.is_editmode:
|
||||
@@ -520,7 +546,16 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return bm
|
||||
|
||||
@classmethod
|
||||
def bmesh_join(cls, bm_a, bm_b, callback=None):
|
||||
def bmesh_join(
|
||||
cls,
|
||||
bm_a: bmesh.types.BMesh,
|
||||
bm_b: bmesh.types.BMesh,
|
||||
callback: Optional[
|
||||
Callable[
|
||||
[bmesh.types.BMesh, list[bmesh.types.BMVert], list[bmesh.types.BMEdge], list[bmesh.types.BMFace]], None
|
||||
]
|
||||
] = None,
|
||||
):
|
||||
"""Join two meshes into single one, store it in `bm_a`"""
|
||||
import bmesh
|
||||
|
||||
@@ -535,7 +570,19 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return bm_a
|
||||
|
||||
@classmethod
|
||||
def toggle_edit_mode(cls, context):
|
||||
def bmesh_check_vertex_in_groups(
|
||||
cls, vertex: bmesh.types.BMVert, deform_layer: bmesh.types.BMLayerItem, groups: list[int]
|
||||
) -> Union[tuple[Literal[True], int], tuple[Literal[False], None]]:
|
||||
"""returns tuple boolean (whether vertex is in any of the groups) and related group index"""
|
||||
for group_index in vertex[deform_layer].keys():
|
||||
# ignore vertex groups assignments produced by edge subdivision near arcs
|
||||
# they usually have weight = 0.5
|
||||
if group_index in groups and vertex[deform_layer][group_index] == 1.0:
|
||||
return True, group_index
|
||||
return False, None
|
||||
|
||||
@classmethod
|
||||
def toggle_edit_mode(cls, context: bpy.types.Context) -> set:
|
||||
ao = context.active_object
|
||||
if not ao:
|
||||
return {"CANCELLED"}
|
||||
@@ -547,21 +594,21 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
return {"CANCELLED"}
|
||||
|
||||
@classmethod
|
||||
def is_object_an_ifc_class(cls, obj, classes):
|
||||
def is_object_an_ifc_class(cls, obj: bpy.types.Object, classes: Iterable[str]) -> bool:
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return element and element.is_a() in classes
|
||||
|
||||
@classmethod
|
||||
def get_object_from_guid(cls, guid):
|
||||
def get_object_from_guid(cls, guid: str) -> Union[IFC_CONNECTED_TYPE, None]:
|
||||
element = tool.Ifc.get().by_guid(guid)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def lock_transform(cls, obj, lock_state=True):
|
||||
def lock_transform(cls, obj: bpy.types.ObjectBase, lock_state=True) -> None:
|
||||
for prop in ("lock_location", "lock_rotation", "lock_scale"):
|
||||
attr = getattr(obj, prop)
|
||||
for axis_idx in range(3):
|
||||
@@ -570,7 +617,9 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
operator_invoke_filepath_hotkeys_description = "Hold Shift to open the file, Alt to browse containing directory"
|
||||
|
||||
@classmethod
|
||||
def operator_invoke_filepath_hotkeys(cls, operator, context, event, filepath: Path):
|
||||
def operator_invoke_filepath_hotkeys(
|
||||
cls, operator: bpy.types.Operator, context: bpy.types.Context, event: bpy.types.Event, filepath: Path
|
||||
) -> Union[set, None]:
|
||||
if not event.alt and not event.shift:
|
||||
return
|
||||
|
||||
@@ -611,6 +660,35 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
open_file_or_folder(filepath.as_posix())
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
@classmethod
|
||||
def get_layer_collection(
|
||||
cls, collection: bpy.types.Collection, view_layer: Optional[bpy.types.ViewLayer] = None
|
||||
) -> Union[bpy.types.LayerCollection, None]:
|
||||
return cls.get_layer_collections_mapping([collection], view_layer).get(collection)
|
||||
|
||||
@classmethod
|
||||
def get_layer_collections_mapping(
|
||||
cls, collections: list[bpy.types.Collection], view_layer: Optional[bpy.types.ViewLayer] = None
|
||||
) -> dict[bpy.types.Collection, bpy.types.LayerCollection]:
|
||||
if view_layer is None:
|
||||
view_layer = bpy.context.view_layer
|
||||
|
||||
collections = list(collections) # copy to prevent mutation
|
||||
collections_mapping = dict()
|
||||
queue = [view_layer.layer_collection]
|
||||
|
||||
while queue:
|
||||
layer = queue.pop()
|
||||
collection = layer.collection
|
||||
if collection in collections:
|
||||
collections_mapping[collection] = layer
|
||||
collections.remove(collection)
|
||||
if not collections:
|
||||
break
|
||||
queue.extend(list(layer.children))
|
||||
|
||||
return collections_mapping
|
||||
|
||||
class Modifier:
|
||||
@classmethod
|
||||
def is_eligible_for_railing_modifier(cls, obj):
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 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 os
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
from contextlib import contextmanager
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
class Clash(blenderbim.core.tool.Clash):
|
||||
|
||||
@classmethod
|
||||
def export_clash_sets(cls):
|
||||
clash_sets = []
|
||||
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets:
|
||||
a = []
|
||||
b = []
|
||||
for ab in ["a", "b"]:
|
||||
for data in getattr(clash_set, ab):
|
||||
clash_source = {"file": data.name}
|
||||
query = tool.Search.export_filter_query(data.filter_groups)
|
||||
if query and data.mode != "a":
|
||||
clash_source["selector"] = query
|
||||
clash_source["mode"] = data.mode
|
||||
if ab == "a":
|
||||
a.append(clash_source)
|
||||
elif ab == "b":
|
||||
b.append(clash_source)
|
||||
clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b}
|
||||
if clash_set.mode == "intersection":
|
||||
clash_set_data["tolerance"] = clash_set.tolerance
|
||||
clash_set_data["check_all"] = clash_set.check_all
|
||||
elif clash_set.mode == "collision":
|
||||
clash_set_data["allow_touching"] = clash_set.allow_touching
|
||||
elif clash_set.mode == "clearance":
|
||||
clash_set_data["clearance"] = clash_set.clearance
|
||||
clash_set_data["check_all"] = clash_set.check_all
|
||||
clash_sets.append(clash_set_data)
|
||||
return clash_sets
|
||||
|
||||
@classmethod
|
||||
def get_clash(cls, clash_set, a_global_id, b_global_id):
|
||||
clashes = clash_set.get("clashes", None)
|
||||
if not clashes:
|
||||
return
|
||||
return clashes.get(f"{a_global_id}-{b_global_id}", None)
|
||||
|
||||
@classmethod
|
||||
def get_clash_set(cls, name):
|
||||
for clash_set in ClashStore.clash_sets:
|
||||
if clash_set["name"] == name:
|
||||
return clash_set
|
||||
|
||||
@classmethod
|
||||
def get_clash_sets(cls):
|
||||
return ClashStore.clash_sets
|
||||
|
||||
@classmethod
|
||||
def import_active_clashes(cls):
|
||||
clash_set = bpy.context.scene.BIMClashProperties.active_clash_set
|
||||
if not clash_set:
|
||||
return
|
||||
clash_set.clashes.clear()
|
||||
result = tool.Clash.get_clash_set(clash_set.name)
|
||||
for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]):
|
||||
blender_clash = clash_set.clashes.add()
|
||||
blender_clash.a_global_id = clash["a_global_id"]
|
||||
blender_clash.b_global_id = clash["b_global_id"]
|
||||
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
|
||||
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
|
||||
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
|
||||
|
||||
@classmethod
|
||||
def load_clash_sets(cls, fn):
|
||||
with open(fn) as f:
|
||||
ClashStore.clash_sets = json.load(f)
|
||||
|
||||
@classmethod
|
||||
def look_at(cls, target, location):
|
||||
camera_location = location
|
||||
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
|
||||
region = next(region for region in area.regions if region.type == "WINDOW")
|
||||
space = next(space for space in area.spaces if space.type == "VIEW_3D")
|
||||
override = {"area": area, "region": region, "space_data": space}
|
||||
space.region_3d.view_location = target
|
||||
space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y")
|
||||
space.region_3d.view_distance = (camera_location - target).length
|
||||
space.shading.show_xray = True
|
||||
|
||||
|
||||
class ClashStore:
|
||||
clash_sets = None
|
||||
path = None
|
||||
|
||||
@staticmethod
|
||||
def purge():
|
||||
ClashStore.clash_sets = None
|
||||
ClashStore.path = None
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import collections
|
||||
import bpy
|
||||
import math
|
||||
import json
|
||||
@@ -27,27 +28,31 @@ import shutil
|
||||
import logging
|
||||
import shapely
|
||||
import platform
|
||||
from shapely.ops import unary_union
|
||||
import mathutils
|
||||
import subprocess
|
||||
import numpy as np
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.selector
|
||||
import blenderbim.bim.import_ifc
|
||||
import blenderbim.bim.module.drawing.sheeter as sheeter
|
||||
import blenderbim.bim.module.drawing.scheduler as scheduler
|
||||
import blenderbim.bim.module.drawing.annotation as annotation
|
||||
import blenderbim.bim.module.drawing.helper as helper
|
||||
import blenderbim.core.root
|
||||
from shapely.ops import unary_union
|
||||
from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData
|
||||
from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS, ANNOTATION_TYPES_DATA
|
||||
from lxml import etree
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
from fractions import Fraction
|
||||
import collections
|
||||
from typing import Optional, Union
|
||||
from typing import Optional, Union, Iterable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Drawing(blenderbim.core.tool.Drawing):
|
||||
@@ -247,7 +252,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
bpy.data.collections.remove(collection, do_unlink=True)
|
||||
|
||||
@classmethod
|
||||
def delete_drawing_elements(cls, elements):
|
||||
def delete_drawing_elements(cls, elements: Iterable[ifcopenshell.entity_instance]) -> None:
|
||||
for element in elements:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
@@ -258,7 +263,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
tool.Blender.remove_data_block(obj_data)
|
||||
|
||||
@classmethod
|
||||
def delete_object(cls, obj):
|
||||
def delete_object(cls, obj: bpy.types.Object) -> None:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
@classmethod
|
||||
@@ -366,7 +371,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_annotation_context(cls, target_view, object_type=None):
|
||||
def get_annotation_context(
|
||||
cls, target_view: str, object_type: Optional[str] = None
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
# checking PLAN target view and annotation type that doesn't require 3d
|
||||
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in (
|
||||
"FALL",
|
||||
@@ -381,7 +388,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
@classmethod
|
||||
def get_document_uri(cls, document, description=None):
|
||||
def get_document_uri(
|
||||
cls, document: ifcopenshell.entity_instance, description: Optional[str] = None
|
||||
) -> Union[str, None]:
|
||||
if getattr(document, "Location", None):
|
||||
if os.path.isabs(document.Location):
|
||||
return document.Location
|
||||
@@ -426,7 +435,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return rel.RelatingGroup
|
||||
|
||||
@classmethod
|
||||
def get_drawing_document(cls, drawing):
|
||||
def get_drawing_document(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
for rel in drawing.HasAssociations:
|
||||
if rel.is_a("IfcRelAssociatesDocument"):
|
||||
return rel.RelatingDocument
|
||||
@@ -440,7 +449,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def get_drawing_target_view(cls, drawing):
|
||||
def get_drawing_target_view(cls, drawing: ifcopenshell.entity_instance) -> str:
|
||||
return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("TargetView", "MODEL_VIEW")
|
||||
|
||||
@classmethod
|
||||
@@ -593,7 +602,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return rel.RelatingProduct
|
||||
|
||||
@classmethod
|
||||
def import_annotations_in_group(cls, group):
|
||||
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
|
||||
elements = set(
|
||||
[e for e in cls.get_group_elements(group) if e.is_a("IfcAnnotation") and e.ObjectType != "DRAWING"]
|
||||
)
|
||||
@@ -603,6 +612,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
ifc_importer.file = tool.Ifc.get()
|
||||
ifc_importer.calculate_unit_scale()
|
||||
ifc_importer.process_context_filter()
|
||||
ifc_importer.load_existing_meshes()
|
||||
ifc_importer.material_creator.load_existing_materials()
|
||||
ifc_importer.create_generic_elements(elements)
|
||||
for obj in ifc_importer.added_data.values():
|
||||
@@ -612,7 +622,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
# but BIMCameraProperties are only synced with EPsetDrawing at drawing import
|
||||
# therefore camera props can differ from pset if the user changed them from pset.
|
||||
@classmethod
|
||||
def import_drawing(cls, drawing):
|
||||
def import_drawing(cls, drawing: ifcopenshell.entity_instance) -> bpy.types.Object:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set(settings.STRICT_TOLERANCE, True)
|
||||
|
||||
@@ -865,7 +875,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
should_add_representation=True,
|
||||
context=None,
|
||||
ifc_representation_class=None,
|
||||
):
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
@@ -1051,7 +1061,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
shutil.copy(ootb_resource, resource_path)
|
||||
|
||||
@classmethod
|
||||
def get_potential_reference_elements(cls, drawing):
|
||||
def get_potential_reference_elements(
|
||||
cls, drawing: ifcopenshell.entity_instance
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
elements = []
|
||||
existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
|
||||
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
||||
@@ -1072,7 +1084,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return elements
|
||||
|
||||
@classmethod
|
||||
def get_drawing_reference_annotation(cls, drawing, reference_element):
|
||||
def get_drawing_reference_annotation(
|
||||
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, bool, None]:
|
||||
if drawing == reference_element:
|
||||
return True
|
||||
for element in cls.get_group_elements(cls.get_drawing_group(drawing)):
|
||||
@@ -1088,26 +1102,50 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if reference_element.is_a("IfcGridAxis"):
|
||||
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
|
||||
|
||||
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
||||
|
||||
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
|
||||
obj = tool.Ifc.get_object(drawing)
|
||||
if obj is None:
|
||||
obj = cls.import_drawing(drawing)
|
||||
# there is no need for other drawing annotations in that case
|
||||
# but we import them anyway so we won't break that drawing activation
|
||||
cls.import_annotations_in_group(cls.get_drawing_group(drawing))
|
||||
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
|
||||
|
||||
psets = ifcopenshell.util.element.get_psets(reference_element)
|
||||
target_view = psets.get("EPset_Drawing", {}).get("TargetView", None)
|
||||
if target_view == "ELEVATION_VIEW":
|
||||
ensure_referenced_drawing_obj_exists(reference_element)
|
||||
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
|
||||
elif target_view == "SECTION_VIEW":
|
||||
ensure_referenced_drawing_obj_exists(reference_element)
|
||||
return cls.generate_section_reference_annotation(drawing, reference_element, context)
|
||||
|
||||
elif reference_element.is_a("IfcBuildingStorey"):
|
||||
return cls.generate_storey_annotation(drawing, reference_element, context)
|
||||
|
||||
@classmethod
|
||||
def generate_storey_annotation(cls, drawing, reference_element, context):
|
||||
def generate_storey_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
|
||||
def to_camera_coords(camera, reference_obj):
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
@@ -1162,13 +1200,18 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_section_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_section_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
reference_obj.matrix_world
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
|
||||
|
||||
def to_camera_coords(camera, reference_obj):
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
@@ -1179,7 +1222,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
mat.translation += annotation_offset
|
||||
return mat
|
||||
|
||||
def clip_to_camera_boundary(mesh, bounds):
|
||||
def clip_to_camera_boundary(mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float]) -> Union[bpy.types.Mesh, None]:
|
||||
mesh.verts.ensure_lookup_table()
|
||||
points = [v.co for v in mesh.verts[0:2]]
|
||||
points = helper.clip_segment(bounds, points)
|
||||
@@ -1240,12 +1283,17 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_elevation_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_elevation_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
reference_obj.matrix_world
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
|
||||
def to_camera_coords(camera, reference_obj):
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
@@ -1272,7 +1320,12 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def generate_grid_axis_reference_annotation(cls, drawing, reference_element, context):
|
||||
def generate_grid_axis_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
@@ -1300,7 +1353,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
mesh.to_mesh(obj.data)
|
||||
return obj
|
||||
|
||||
def to_camera_coords(obj, mesh):
|
||||
def to_camera_coords(obj: bpy.types.Object, mesh: bpy.types.Mesh) -> tuple[bpy.types.Object, bpy.types.Mesh]:
|
||||
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
|
||||
obj.matrix_world = camera.matrix_world
|
||||
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
||||
@@ -1357,7 +1410,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def is_perpendicular(cls, a, b):
|
||||
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
|
||||
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
||||
a_quaternion = a.matrix_world.to_quaternion()
|
||||
b_quaternion = b.matrix_world.to_quaternion()
|
||||
@@ -1367,7 +1420,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_camera_block(cls, obj):
|
||||
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
|
||||
raster_x = obj.data.BIMCameraProperties.raster_x
|
||||
raster_y = obj.data.BIMCameraProperties.raster_y
|
||||
|
||||
@@ -1400,7 +1453,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return {"verts": verts, "faces": faces}
|
||||
|
||||
@classmethod
|
||||
def is_intersecting(cls, a, b):
|
||||
def is_intersecting(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
|
||||
a_block = cls.get_camera_block(a)
|
||||
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
|
||||
b_block = cls.get_camera_block(b)
|
||||
@@ -1622,7 +1675,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
bpy.ops.bim.activate_model()
|
||||
|
||||
@classmethod
|
||||
def activate_drawing(cls, camera):
|
||||
def activate_drawing(cls, camera: bpy.types.Object) -> None:
|
||||
area = tool.Blender.get_view3d_area()
|
||||
is_local_view = area.spaces[0].local_view is not None
|
||||
if is_local_view:
|
||||
@@ -1661,6 +1714,8 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
|
||||
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
|
||||
filtered_elements.add(drawing)
|
||||
|
||||
# hide non-filtered elemenets
|
||||
for view_layer_object in bpy.context.view_layer.objects:
|
||||
element = tool.Ifc.get_entity(view_layer_object)
|
||||
if not element or element.is_a("IfcTypeProduct"):
|
||||
@@ -1669,7 +1724,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
view_layer_object.hide_set(hide)
|
||||
view_layer_object.hide_render = hide
|
||||
|
||||
subcontexts = []
|
||||
subcontexts: list[ifcopenshell.entity_instance] = []
|
||||
target_view = cls.get_drawing_target_view(drawing)
|
||||
|
||||
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
|
||||
@@ -1702,6 +1757,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
if subcontext:
|
||||
subcontexts.append(context_filter)
|
||||
|
||||
# switch representations and hide elements without representations
|
||||
for element in filtered_elements:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
current_representation = tool.Geometry.get_active_representation(obj)
|
||||
@@ -1730,7 +1786,9 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
has_context = True
|
||||
break
|
||||
|
||||
if not has_context:
|
||||
# don't hide IfcAnnotations as some of them might exist without representations
|
||||
# e.g. with ObjectType = SYMBOL
|
||||
if not has_context and not element.is_a("IfcAnnotation"):
|
||||
obj.hide_set(True)
|
||||
obj.hide_render = True
|
||||
|
||||
@@ -1937,3 +1995,60 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
rotate180z = mathutils.Matrix.Rotation(math.radians(180.0), 4, "Z")
|
||||
return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4() @ rotate180z
|
||||
return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
|
||||
|
||||
@classmethod
|
||||
def convert_svg_to_dxf(cls, svg_filepath: Path, dxf_filepath: Path) -> None:
|
||||
import ezdxf
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
IFC = "{http://www.ifcopenshell.org/ns}"
|
||||
|
||||
doc = ezdxf.new("R2010")
|
||||
msp = doc.modelspace()
|
||||
svg = ET.parse(svg_filepath).getroot()
|
||||
|
||||
def finalize_dxf():
|
||||
doc.saveas(dxf_filepath)
|
||||
|
||||
drawing = svg.findall(f"{SVG}g[@{IFC}name]")
|
||||
if not drawing:
|
||||
finalize_dxf()
|
||||
return
|
||||
drawing = drawing[0]
|
||||
|
||||
NUMBER = r"-?\d+\.?\d+"
|
||||
COORD = rf"{NUMBER},{NUMBER}"
|
||||
POLYLINE_PATTERN = rf"M{COORD} (?:L{COORD} ?)+Z? ?"
|
||||
MULTI_POLYLINE_PATTERN = rf"^({POLYLINE_PATTERN})+$"
|
||||
|
||||
for element_g in drawing.findall(f"{SVG}g"):
|
||||
paths = element_g.findall(f"{SVG}path")
|
||||
|
||||
for path in paths:
|
||||
path = path.attrib["d"]
|
||||
|
||||
if not re.match(MULTI_POLYLINE_PATTERN, path):
|
||||
# print(f'Path "{path}" doesn\'t match expected pattern {MULTI_POLYLINE_PATTERN}')
|
||||
continue
|
||||
|
||||
for polyline_path in re.findall(POLYLINE_PATTERN, path):
|
||||
points = re.findall(rf"{NUMBER}", polyline_path)
|
||||
points = [float(p) for p in points]
|
||||
POINT_SIZE = 2
|
||||
|
||||
grouped_points = []
|
||||
for i in range(0, len(points), POINT_SIZE):
|
||||
point = points[i : i + POINT_SIZE]
|
||||
point[1] *= -1
|
||||
grouped_points.append(point)
|
||||
points = grouped_points
|
||||
|
||||
# Z marks closed polylines
|
||||
is_closed_polyline = polyline_path.rstrip().endswith("Z")
|
||||
if is_closed_polyline or len(points) > 2:
|
||||
msp.add_lwpolyline(points, close=is_closed_polyline)
|
||||
else: # LINE
|
||||
msp.add_line(*points)
|
||||
|
||||
finalize_dxf()
|
||||
|
||||
@@ -32,7 +32,7 @@ import blenderbim.bim.import_ifc
|
||||
from math import radians, pi
|
||||
from mathutils import Vector, Matrix
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
class Geometry(blenderbim.core.tool.Geometry):
|
||||
@@ -315,7 +315,9 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
@classmethod
|
||||
def get_subcontext_parameters(cls, subcontext):
|
||||
def get_subcontext_parameters(
|
||||
cls, subcontext: ifcopenshell.entity_instance
|
||||
) -> tuple[Union[str, None], Union[str, None], Union[str, None]]:
|
||||
return (
|
||||
subcontext.ContextType,
|
||||
subcontext.ContextIdentifier,
|
||||
|
||||
@@ -79,7 +79,7 @@ class Ifc(blenderbim.core.tool.Ifc):
|
||||
return IfcStore.get_schema()
|
||||
|
||||
@classmethod
|
||||
def get_entity(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
|
||||
def get_entity(cls, obj: IFC_CONNECTED_TYPE) -> ifcopenshell.entity_instance:
|
||||
ifc = IfcStore.get_file()
|
||||
props = getattr(obj, "BIMObjectProperties", None)
|
||||
if ifc and props and props.ifc_definition_id:
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import collections
|
||||
import collections.abc
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
@@ -36,23 +37,26 @@ from blenderbim.bim import import_ifc
|
||||
from blenderbim.bim.module.geometry.helper import Helper
|
||||
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
|
||||
from ifcopenshell.util.shape_builder import V, ShapeBuilder
|
||||
from typing import Optional, Union, TypeVar, Any
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Model(blenderbim.core.tool.Model):
|
||||
@classmethod
|
||||
def convert_si_to_unit(cls, value):
|
||||
def convert_si_to_unit(cls, value: T) -> T:
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v / cls.unit_scale for v in value]
|
||||
return value / cls.unit_scale
|
||||
|
||||
@classmethod
|
||||
def convert_unit_to_si(cls, value):
|
||||
def convert_unit_to_si(cls, value: T) -> T:
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v * cls.unit_scale for v in value]
|
||||
return value * cls.unit_scale
|
||||
|
||||
@classmethod
|
||||
def convert_data_to_project_units(cls, data, non_si_props=[]):
|
||||
def convert_data_to_project_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for prop_name in data:
|
||||
if prop_name in non_si_props:
|
||||
@@ -65,7 +69,7 @@ class Model(blenderbim.core.tool.Model):
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def convert_data_to_si_units(cls, data, non_si_props=[]):
|
||||
def convert_data_to_si_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for prop_name in data:
|
||||
if prop_name in non_si_props:
|
||||
@@ -78,34 +82,35 @@ class Model(blenderbim.core.tool.Model):
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def export_curve(cls, position, edge_indices, points=None):
|
||||
def export_curve(cls, position: Matrix, edge_indices: list[tuple[int, int]]) -> ifcopenshell.entity_instance:
|
||||
position_i = position.inverted()
|
||||
ifc_file = tool.Ifc.get()
|
||||
if len(edge_indices) == 2:
|
||||
diameter = edge_indices[0]
|
||||
p1 = cls.bm.verts[diameter[0]].co
|
||||
p2 = cls.bm.verts[diameter[1]].co
|
||||
center = cls.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
|
||||
radius = cls.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
|
||||
return ifc_file.createIfcCircle(
|
||||
ifc_file.createIfcAxis2Placement2D(ifc_file.createIfcCartesianPoint(center[0:2])), radius
|
||||
)
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
points = []
|
||||
for edge in edge_indices:
|
||||
local_point = (position_i @ Vector(cls.bm.verts[edge[0]].co)).to_2d()
|
||||
points.append(tool.Ifc.get().createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
|
||||
points.append(ifc_file.createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
|
||||
points.append(points[0])
|
||||
return tool.Ifc.get().createIfcPolyline(points)
|
||||
return ifc_file.createIfcPolyline(points)
|
||||
segments = []
|
||||
for segment in edge_indices:
|
||||
if len(segment) == 2:
|
||||
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
|
||||
segments.append(ifc_file.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(cls.points, segments, False)
|
||||
segments.append(ifc_file.createIfcArcIndex([i + 1 for i in segment]))
|
||||
return ifc_file.createIfcIndexedPolyCurve(cls.points, segments, False)
|
||||
|
||||
@classmethod
|
||||
def export_points(cls, position, indices):
|
||||
def export_points(cls, position: Matrix, indices: list[Vector]) -> ifcopenshell.entity_instance:
|
||||
position_i = position.inverted()
|
||||
points = []
|
||||
for point in indices:
|
||||
@@ -114,7 +119,10 @@ class Model(blenderbim.core.tool.Model):
|
||||
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
|
||||
@classmethod
|
||||
def export_profile(cls, obj, position=None):
|
||||
def export_profile(
|
||||
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Returns `None` in case if profile was invalid."""
|
||||
if position is None:
|
||||
position = Matrix()
|
||||
|
||||
@@ -150,7 +158,7 @@ class Model(blenderbim.core.tool.Model):
|
||||
return profile
|
||||
|
||||
@classmethod
|
||||
def export_surface(cls, obj):
|
||||
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
|
||||
|
||||
edge1 = p2 - p1
|
||||
@@ -202,7 +210,7 @@ class Model(blenderbim.core.tool.Model):
|
||||
return surface
|
||||
|
||||
@classmethod
|
||||
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str):
|
||||
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str) -> str:
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
if props.occurrence_name_style == "CLASS":
|
||||
return ifc_class[3:]
|
||||
@@ -216,7 +224,8 @@ class Model(blenderbim.core.tool.Model):
|
||||
return "Instance"
|
||||
|
||||
@classmethod
|
||||
def get_extrusion(cls, representation):
|
||||
def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""return first found IfcExtrudedAreaSolid"""
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
|
||||
@@ -23,14 +23,17 @@ import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
from mathutils import Vector
|
||||
from ifcopenshell import util
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes
|
||||
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
|
||||
import blenderbim.bim.schema
|
||||
from typing import Optional, Union, Literal
|
||||
|
||||
|
||||
class Qto(blenderbim.core.tool.Qto):
|
||||
@classmethod
|
||||
def get_radius_of_selected_vertices(cls, obj):
|
||||
def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float:
|
||||
selected_verts = [v.co for v in obj.data.vertices if v.select]
|
||||
total = Vector()
|
||||
for v in selected_verts:
|
||||
@@ -39,16 +42,16 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return max([(v - circle_center).length for v in selected_verts])
|
||||
|
||||
@classmethod
|
||||
def set_qto_result(cls, result):
|
||||
def set_qto_result(cls, result: float) -> None:
|
||||
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
|
||||
|
||||
@classmethod
|
||||
def add_object_base_qto(cls, obj):
|
||||
def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
return cls.add_product_base_qto(product)
|
||||
|
||||
@classmethod
|
||||
def add_product_base_qto(cls, product):
|
||||
def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
base_quantity_name = cls.get_applicable_base_quantity_name(product)
|
||||
if base_quantity_name:
|
||||
return tool.Ifc.run(
|
||||
@@ -58,7 +61,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_quantity_names(cls, qto_name):
|
||||
def get_applicable_quantity_names(cls, qto_name: str) -> list[str]:
|
||||
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name)
|
||||
return (
|
||||
[property.Name for property in pset_template.HasPropertyTemplates]
|
||||
@@ -67,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_applicable_base_quantity_name(cls, product=None):
|
||||
def get_applicable_base_quantity_name(
|
||||
cls, product: Optional[ifcopenshell.entity_instance] = None
|
||||
) -> Union[str, None]:
|
||||
if not product:
|
||||
return
|
||||
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
|
||||
@@ -76,36 +81,44 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
|
||||
|
||||
@classmethod
|
||||
def get_new_calculated_quantity(cls, qto_name, quantity_name, obj):
|
||||
def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
|
||||
return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj)
|
||||
|
||||
@classmethod
|
||||
def get_new_guessed_quantity(cls, obj, quantity_name, alternative_prop_names):
|
||||
def get_new_guessed_quantity(
|
||||
cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str]
|
||||
) -> Union[float, None]:
|
||||
return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj)
|
||||
|
||||
@classmethod
|
||||
def get_rounded_value(cls, new_quantity):
|
||||
def get_rounded_value(cls, new_quantity: float) -> float:
|
||||
return round(new_quantity, 3)
|
||||
|
||||
@classmethod
|
||||
def get_calculated_object_quantities(cls, calculator, qto_name, obj):
|
||||
def get_calculated_object_quantities(
|
||||
cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object
|
||||
) -> dict[str, float]:
|
||||
return {
|
||||
quantity_name: cls.get_rounded_value(calculator.calculate_quantity(qto_name, quantity_name, obj))
|
||||
quantity_name: cls.get_rounded_value(value)
|
||||
for quantity_name in cls.get_applicable_quantity_names(qto_name) or []
|
||||
if cls.has_calculator(qto_name, quantity_name)
|
||||
and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None
|
||||
and (value := calculator.calculate_quantity(qto_name, quantity_name, obj)) is not None
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def has_calculator(cls, qto_name, quantity_name):
|
||||
def has_calculator(cls, qto_name: str, quantity_name: str) -> bool:
|
||||
return bool(mapper.get(qto_name, {}).get(quantity_name, None))
|
||||
|
||||
@classmethod
|
||||
def convert_to_project_units(cls, value, qto_name=None, quantity_name=None, quantity_type=None):
|
||||
def convert_to_project_units(
|
||||
cls,
|
||||
value: float,
|
||||
qto_name: Optional[str] = None,
|
||||
quantity_name: Optional[str] = None,
|
||||
quantity_type: Optional[QuanityTypes] = None,
|
||||
) -> Union[float, None]:
|
||||
"""You can either specify `quantity_type` or provide `qto_name/quantity_name`
|
||||
to let method figure the `quantity_type` from the templates
|
||||
|
||||
`quantity_type` values are `Q_LENGTH`, `Q_AREA`, `Q_VOLUME`
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
quantity_to_unit_types = {
|
||||
@@ -135,7 +148,9 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_guessed_quantities(cls, obj, pset_qto_properties):
|
||||
def get_guessed_quantities(
|
||||
cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance]
|
||||
) -> dict[str, float]:
|
||||
calculated_quantities = {}
|
||||
for pset_qto_property in pset_qto_properties:
|
||||
quantity_name = pset_qto_property.get_info()["Name"]
|
||||
@@ -153,7 +168,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return calculated_quantities
|
||||
|
||||
@classmethod
|
||||
def get_base_qto(cls, product):
|
||||
def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not hasattr(product, "IsDefinedBy"):
|
||||
return
|
||||
for rel in product.IsDefinedBy or []:
|
||||
@@ -166,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
return rel.RelatingPropertyDefinition
|
||||
|
||||
@classmethod
|
||||
def get_related_cost_item_quantities(cls, product):
|
||||
def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]:
|
||||
"""_summary_: Returns the related cost item and related quantities of the product
|
||||
|
||||
:param ifc-instance product: ifc instance
|
||||
|
||||
@@ -21,6 +21,8 @@ import ifcopenshell
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.core.material
|
||||
import blenderbim.core.style
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
@@ -20,10 +20,17 @@ class Search(blenderbim.core.tool.Search):
|
||||
return bpy.context.scene.BIMSearchProperties.filter_groups
|
||||
elif module == "csv":
|
||||
return bpy.context.scene.CsvProperties.filter_groups
|
||||
elif module == "diff":
|
||||
return bpy.context.scene.DiffProperties.filter_groups
|
||||
elif module == "drawing_include":
|
||||
return bpy.context.active_object.data.BIMCameraProperties.include_filter_groups
|
||||
elif module == "drawing_exclude":
|
||||
return bpy.context.active_object.data.BIMCameraProperties.exclude_filter_groups
|
||||
elif module.startswith("clash"):
|
||||
_, clash_set_index, ab, clash_source_index = module.split("_")
|
||||
return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[
|
||||
int(clash_source_index)
|
||||
].filter_groups
|
||||
|
||||
@classmethod
|
||||
def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None:
|
||||
@@ -189,6 +196,8 @@ class ImportFilterQueryTransformer(lark.Transformer):
|
||||
|
||||
def query(self, args):
|
||||
keys, comparison, value = args
|
||||
if comparison == "=":
|
||||
comparison = ""
|
||||
return {"type": "query", "name": keys, "value": f"{comparison}{value}"}
|
||||
|
||||
def comparison(self, args):
|
||||
|
||||
@@ -150,9 +150,9 @@ class Spatial(blenderbim.core.tool.Spatial):
|
||||
for product in products:
|
||||
obj = tool.Ifc.get_object(product)
|
||||
if obj and bpy.context.view_layer.objects.get(obj.name):
|
||||
obj.select_set(True)
|
||||
if unhide:
|
||||
obj.hide_set(False)
|
||||
obj.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def filter_products(cls, products, action):
|
||||
|
||||
@@ -96,7 +96,7 @@ class Style(blenderbim.core.tool.Style):
|
||||
return obj.name
|
||||
|
||||
@classmethod
|
||||
def get_style(cls, obj):
|
||||
def get_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if obj.BIMMaterialProperties.ifc_style_id:
|
||||
try:
|
||||
return tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
|
||||
|
||||
+37
-5
@@ -1,12 +1,44 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap");
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap');
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: normal;
|
||||
color: #7cbf33;
|
||||
background: -webkit-linear-gradient(0deg, #7cbf33, #049344);
|
||||
background-clip: border-box;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.toc-tree .reference:hover {
|
||||
color: #d98014;
|
||||
}
|
||||
.sidebar-brand-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.blockbutton {
|
||||
border: 1px solid var(--color-admonition-title--seealso);
|
||||
background-color: var(--color-admonition-title-background--seealso);
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.blockbutton a {
|
||||
display: block;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-radius: 5px;
|
||||
background: #40b74c;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.blockbutton a:hover {
|
||||
background: #70ba35;
|
||||
}
|
||||
.highlight .hll {
|
||||
background-color: #ffc2;
|
||||
}
|
||||
section img {
|
||||
display: block;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "BlenderBIM Add-on"
|
||||
copyright = "2020-2023 IfcOpenShell Contributors"
|
||||
copyright = "2020-2024 IfcOpenShell Contributors"
|
||||
author = "IfcOpenShell Contributors"
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
@@ -48,7 +48,7 @@ release = "0.0.231104"
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel"]
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel", "sphinx_copybutton"]
|
||||
|
||||
# Auto add document prefixes to help guarantee uniqueness of automatic section references.
|
||||
autosectionlabel_prefix_document = True
|
||||
@@ -75,3 +75,59 @@ html_theme = "furo"
|
||||
html_static_path = ["_static"]
|
||||
|
||||
html_css_files = ["custom.css"]
|
||||
|
||||
# Code block styles. Dark styling helps important code examples "pop" on the
|
||||
# page even on light themes.
|
||||
pygments_style = "one-dark"
|
||||
pygments_dark_style = "one-dark"
|
||||
|
||||
html_logo = "https://blenderbim.org/assets/images/blender/blender-logo.png"
|
||||
html_theme_options = {
|
||||
"source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/",
|
||||
"source_branch": "v0.7.0",
|
||||
"source_directory": "src/blenderbim/docs/",
|
||||
"light_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#f7f7f6",
|
||||
"color-background-secondary": "#eeeeec",
|
||||
"color-background-border": "#cfd0cb",
|
||||
"color-foreground-primary": "#2e3436",
|
||||
"color-sidebar-item-background--hover": "#f7f7f6",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
"dark_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#2e3436",
|
||||
"color-background-border": "#2e3436",
|
||||
"color-foreground-primary": "#eeeeec",
|
||||
"color-sidebar-item-background--hover": "#2e3436",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
|
||||
"footer_icons": [
|
||||
{
|
||||
"name": "IfcOpenShell",
|
||||
"url": "https://ifcopenshell.org",
|
||||
"html": """
|
||||
<img src="https://ifcopenshell.org/assets/images/logo.png" style="width: auto;" />
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/docs",
|
||||
"html": """
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ You will need to choose which build to download.
|
||||
- If you are on Blender >=4.1, choose py311
|
||||
- If you are on Blender >=3.1 and <=4.0, choose py10
|
||||
- If you are on Blender >=2.93 and <3.1, choose py39
|
||||
- Choose ``linux``, ``macos``, ``macosm1`` (for Apple M1 devices), or ``win``
|
||||
depending on your operating system
|
||||
- Choose ``linux``, ``macos`` (Apple Intel), ``macosm1`` (Apple Silicon), or
|
||||
``win`` depending on your operating system
|
||||
|
||||
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
|
||||
but it happens.
|
||||
@@ -43,11 +43,11 @@ 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``, ``macosm1``, and ``win``.
|
||||
You can choose between a ``PYVERSION`` of ``py39``, or ``py310``.
|
||||
You can choose between a ``PYVERSION`` of ``py311``, ``py310``, or ``py39``.
|
||||
::
|
||||
|
||||
$ cd src/blenderbim
|
||||
$ make dist PLATFORM=linux PYVERSION=py310
|
||||
$ make dist PLATFORM=linux PYVERSION=py311
|
||||
$ ls dist/
|
||||
|
||||
This will give you a fully packaged Blender add-on zip that you can distribute
|
||||
@@ -258,10 +258,12 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
|
||||
ifcclash
|
||||
bimtester
|
||||
ifccobie
|
||||
ifcdiff
|
||||
ifccsv
|
||||
ifcdiff
|
||||
ifc4d
|
||||
ifc5d
|
||||
ifcpatch
|
||||
ifcp6
|
||||
ifctester
|
||||
pystache
|
||||
svgwrite
|
||||
dateutil
|
||||
@@ -276,17 +278,13 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
|
||||
elementpath
|
||||
six
|
||||
lark-parser
|
||||
hppfcl
|
||||
behave
|
||||
parse
|
||||
parse_type
|
||||
xlsxwriter
|
||||
odfpy
|
||||
defusedxml
|
||||
boto3
|
||||
botocore
|
||||
jmespath
|
||||
s3transfer
|
||||
ifcjson
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -6,9 +6,13 @@ the Python version shipped by the Blender Foundation for the most recent three
|
||||
major Blender versions:
|
||||
|
||||
- 64-bit Linux on Python 3.10
|
||||
- 64-bit MacOS on Python 3.10
|
||||
- 64-bit MacOS M1 on Python 3.10
|
||||
- 64-bit Linux on Python 3.11
|
||||
- 64-bit MacOS Intel on Python 3.10
|
||||
- 64-bit MacOS Intel on Python 3.11
|
||||
- 64-bit MacOS Silicon on Python 3.10
|
||||
- 64-bit MacOS Silicon on Python 3.11
|
||||
- 64-bit Windows on Python 3.10
|
||||
- 64-bit Windows on Python 3.11
|
||||
|
||||
Note that developer builds may exist for older versions of Python but there will
|
||||
be no guarantee of the uptime or stability of these builds.
|
||||
|
||||
@@ -12,6 +12,8 @@ Writing technical documentation
|
||||
All documentation is written in ReStructured Text and is available in the
|
||||
`BlenderBIM Add-on docs directory
|
||||
<https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/docs>`_.
|
||||
You can press the edit button on the top right on any documentation page to
|
||||
quickly edit their content.
|
||||
|
||||
You can link to `external websites
|
||||
<https://docs.readthedocs.io/en/stable/guides/cross-referencing-with-sphinx.html>`_.
|
||||
@@ -86,3 +88,27 @@ Tables can be very annoying to format. You can use a CSV table instead.
|
||||
|
||||
"ABC", "01", "02"
|
||||
"DEF", "03", "04"
|
||||
|
||||
Building documentation
|
||||
----------------------
|
||||
|
||||
If you want to build the documentation locally, the documentation system uses
|
||||
`Sphinx <https://www.sphinx-doc.org/en/master/>`_. First, install the theme and
|
||||
theme dependencies:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install furo
|
||||
$ pip install sphinx-autoapi
|
||||
$ pip install sphinx-copybutton
|
||||
|
||||
Now you can generate the documentation:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd /path/to/ifcopenshell/src/blenderbim/docs/
|
||||
$ make html
|
||||
$ cd _build/html
|
||||
$ python -m http.server
|
||||
|
||||
You will now have a local webserver running hosting the documentation.
|
||||
|
||||
@@ -70,8 +70,8 @@ In the top left **File** menu, Save your new IFC model on your computer.
|
||||
Congratulations! You have now created your first OpenBIM model from Blender! You
|
||||
can open the IFC file in any other program, and you will see something similar
|
||||
to the image below. Three simple open source online viewers you can test with
|
||||
are `IfcPipeline <https://view.ifcopenshell.org>`__, `IFC.JS
|
||||
<https://ifcjs.github.io/web-ifc-viewer/example/index>`__, and `3DViewer
|
||||
are `IfcPipeline <https://view.ifcopenshell.org>`__, `ThatOpenEditor
|
||||
<https://platform.thatopen.com/app>`__, and `3DViewer
|
||||
<https://3dviewer.net/>`__.
|
||||
|
||||
.. image:: images/ifc-pipeline.png
|
||||
|
||||
@@ -25,7 +25,7 @@ Installation
|
||||
|
||||
.. warning::
|
||||
|
||||
If you are not using Blender version >=3.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>`
|
||||
If you are not using Blender version >=4.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>`
|
||||
|
||||
3. **Install the BlenderBIM Add-on**
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,138 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 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 os
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import blenderbim.tool as tool
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# When run from Blender
|
||||
BLEND_DIR = os.path.dirname(bpy.data.filepath)
|
||||
OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Entourage Library.ifc")
|
||||
|
||||
|
||||
class LibraryGenerator:
|
||||
def generate(self, library_name, output_filename):
|
||||
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=library_name)
|
||||
self.library = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
|
||||
)
|
||||
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", 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 = {
|
||||
"Model/Body/MODEL_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="MODEL_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"Plan/Body/PLAN_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Plan",
|
||||
context_identifier="Body",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=plan,
|
||||
),
|
||||
"Model/Body/PLAN_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"Model/Body/SECTION_VIEW": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="SECTION_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
}
|
||||
|
||||
# Manually modeled trees
|
||||
for obj in bpy.data.objects:
|
||||
if not obj.type == "MESH":
|
||||
continue
|
||||
if "Plan/" in obj.name or "Model/" in obj.name:
|
||||
continue
|
||||
representations = {"Model/Body/MODEL_VIEW": obj.name}
|
||||
for rep_key in self.representations.keys():
|
||||
rep_obj = bpy.data.objects.get(obj.name + " " + rep_key)
|
||||
if rep_obj:
|
||||
representations[rep_key] = rep_obj.name
|
||||
self.create_type("IfcBuildingElementProxyType", obj.name, representations)
|
||||
|
||||
self.file.write(output_filename)
|
||||
|
||||
def create_type(self, ifc_class, name, representations):
|
||||
element = ifcopenshell.api.run(
|
||||
"root.create_entity", self.file, ifc_class=ifc_class, predefined_type="ENTOURAGE", 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="IfcSurfaceStyleShading",
|
||||
attributes=tool.Style.get_surface_shading_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("Entourage Assets Library", output_filename=OUT_PATH)
|
||||
@@ -438,7 +438,7 @@ Scenario: Override duplicate move - copying an aggregate
|
||||
And the object "IfcElementAssembly/Assembly.001" exists
|
||||
And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001"
|
||||
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey"
|
||||
|
||||
|
||||
Scenario: Override duplicate move - copying objects with connection
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
@@ -467,7 +467,7 @@ Scenario: Override duplicate move - copying objects with connection
|
||||
Then the object "IfcSlab/Slab.001" exists
|
||||
And the variable "slab_name" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcSlab/Slab.001'][0]"
|
||||
Then the object "{wall_name}" has a connection with "{slab_name}"
|
||||
|
||||
|
||||
Scenario: Override duplicate move - copying walls with mitre joint
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
@@ -477,24 +477,24 @@ Scenario: Override duplicate move - copying walls with mitre joint
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
When I deselect all objects
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I duplicate the selected objects
|
||||
Then the object "IfcWall/Wall.002" exists
|
||||
And the variable "wall_name1" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.002'][0]"
|
||||
Then the object "IfcWall/Wall.003" exists
|
||||
And the variable "wall_name2" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.003'][0]"
|
||||
Then the object "{wall_name1}" has a connection with "{wall_name2}"
|
||||
|
||||
|
||||
Scenario: Override duplicate move linked - without active IFC data
|
||||
Given an empty Blender session
|
||||
And I add a cube
|
||||
@@ -544,14 +544,14 @@ Scenario: Override paste buffer - with active IFC data
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
When I press "view3d.copybuffer"
|
||||
# IFC elements unlinked on paste for safety
|
||||
And I press "bim.override_paste_buffer"
|
||||
Then the object "IfcWall/Cube" exists
|
||||
And the object "IfcWall/Cube" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" exists
|
||||
And the object "IfcWall/Cube.001" is an "IfcWall"
|
||||
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
And the object "IfcBuildingStorey/My Storey.001" exists
|
||||
And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey"
|
||||
And the object "Cube.001" exists
|
||||
And the object "Cube.001" is not an IFC element
|
||||
And the object "My Storey.001" exists
|
||||
And the object "My Storey.001" is not an IFC element
|
||||
|
||||
Scenario: Duplicate linked aggregate
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
@@ -559,8 +559,8 @@ Scenario: Duplicate linked aggregate
|
||||
When I duplicate linked aggregate the selected objects
|
||||
Then the object "IfcWall/Wall_01.001" exists
|
||||
And the object "IfcWall/Wall_02.001" exists
|
||||
And the object "IfcElementAssembly/Assembly_02" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_02" belong to the same Linked Aggregate Group
|
||||
And the object "IfcElementAssembly/Assembly_01" exists
|
||||
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_01" belong to the same Linked Aggregate Group
|
||||
|
||||
Scenario: Refresh linked aggregate
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
@@ -588,7 +588,7 @@ Scenario: Refresh linked aggregate - after deleting an object
|
||||
When I refresh linked aggregate the selected object
|
||||
Then the object "IfcWall/Wall_01" does not exist
|
||||
And the object "IfcWall/Wall_02" exists
|
||||
|
||||
|
||||
Scenario: Refresh linked aggregate - after duplicating an object
|
||||
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
|
||||
And the object "IfcWall/Wall_01" is selected
|
||||
@@ -606,4 +606,4 @@ Scenario: Refresh linked aggregate - after duplicating an object
|
||||
Then the object "IfcWall/Wall_01.001" exists
|
||||
And the object "IfcWall/Wall_02.001" exists
|
||||
And the object "IfcWall/Wall_03.001" exists
|
||||
|
||||
|
||||
|
||||
@@ -107,11 +107,11 @@ Scenario: Add a wall perpendicular to an existing wall
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
When I press "bim.hotkey(hotkey='S_A')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Extend one wall to another
|
||||
Given an empty IFC project
|
||||
@@ -122,35 +122,17 @@ Scenario: Extend one wall to another
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
And the object "IfcWall/Wall.001" is moved to "0.5,-1,0"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_E')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-2,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "2,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-2,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - first wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -162,13 +144,13 @@ Scenario: Join two walls with a butt joint - second wall has priority
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.4,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.6,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Scenario: Join two walls with a butt joint - second wall has priority
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
@@ -179,12 +161,30 @@ Scenario: Join two walls with a mitre joint
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And additionally the object "IfcWall/Wall.001" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
|
||||
When I press "bim.hotkey(hotkey='S_T')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Join two walls with a mitre joint
|
||||
Given an empty IFC project
|
||||
And I load the demo construction library
|
||||
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
|
||||
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
|
||||
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the cursor is at "0.5,0,0"
|
||||
And I press "bim.hotkey(hotkey='S_A')"
|
||||
And the object "IfcWall/Wall.001" is selected
|
||||
And additionally the object "IfcWall/Wall" is selected
|
||||
When I press "bim.hotkey(hotkey='S_Y')"
|
||||
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
|
||||
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
|
||||
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
|
||||
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
|
||||
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
|
||||
|
||||
Scenario: Change the height of a wall
|
||||
Given an empty IFC project
|
||||
|
||||
@@ -313,49 +313,65 @@ Scenario: Unload project
|
||||
|
||||
Scenario: Link IFC
|
||||
Given an empty IFC project
|
||||
When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "True"
|
||||
And the object "IfcWall/Wall" exists
|
||||
And the object "IfcSlab/Slab" exists
|
||||
And the object "IfcElementAssembly/Empty" exists
|
||||
And the object "IfcBeam/Beam" exists
|
||||
And the object "IfcBuildingStorey/Ground Floor" exists
|
||||
And the object "IfcBuildingStorey/Level 1" exists
|
||||
When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
|
||||
And the collection "IfcProject/basic.ifc" exists
|
||||
And the object "Chunk" exists
|
||||
And the object "Chunk" is placed in the collection "IfcProject/basic.ifc"
|
||||
|
||||
Scenario: Toggle link visibility - wireframe mode
|
||||
Given an empty IFC project
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
|
||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.blend', mode='WIREFRAME')"
|
||||
Then nothing happens
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "True"
|
||||
And the object "Chunk" should display as "WIRE"
|
||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "False"
|
||||
And the object "Chunk" should display as "TEXTURED"
|
||||
|
||||
Scenario: Toggle link selectability
|
||||
Given an empty IFC project
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "False"
|
||||
And the collection "IfcProject/basic.ifc" is unselectable
|
||||
When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "True"
|
||||
And the collection "IfcProject/basic.ifc" is selectable
|
||||
|
||||
Scenario: Toggle link visibility - visible mode
|
||||
Given an empty IFC project
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
|
||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.blend', mode='VISIBLE')"
|
||||
Then nothing happens
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True"
|
||||
And the collection "IfcProject/basic.ifc" exclude status is "True"
|
||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False"
|
||||
And the collection "IfcProject/basic.ifc" exclude status is "False"
|
||||
|
||||
Scenario: Unload link
|
||||
Given an empty Blender session
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
|
||||
When I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "False"
|
||||
And "scene.collection.children.get('IfcProject/My Project')" is "None"
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
When I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "False"
|
||||
And the collection "IfcProject/basic.ifc" does not exist
|
||||
|
||||
Scenario: Load link
|
||||
Given an empty Blender session
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
|
||||
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')"
|
||||
When I press "bim.load_link(filepath='{cwd}/test/files/basic.blend')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "True"
|
||||
And "scene.collection.children['IfcProject/My Project'].users" is "2"
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
|
||||
When I press "bim.load_link(filepath='{cwd}/test/files/basic.ifc')"
|
||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
|
||||
And the collection "IfcProject/basic.ifc" exists in viewlayer
|
||||
|
||||
Scenario: Unlink IFC
|
||||
Given an empty Blender session
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
|
||||
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')"
|
||||
When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.blend')"
|
||||
Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.blend')" is "None"
|
||||
And "scene.collection.children.get('IfcProject/My Project')" is "None"
|
||||
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
|
||||
When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||
Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.ifc')" is "None"
|
||||
And "scene.collection.children.get('IfcProject/basic.ifc')" is "None"
|
||||
And the object "Chunk" does not exist
|
||||
|
||||
Scenario: Export IFC - blank project
|
||||
Given an empty IFC project
|
||||
|
||||
@@ -387,10 +387,10 @@ Scenario: Edit pset length property
|
||||
|
||||
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
|
||||
Scenario: Edit qset length property
|
||||
Given an empty IFC project
|
||||
And I press "mesh.add_clever_stair"
|
||||
@@ -401,15 +401,14 @@ Scenario: Edit qset length property
|
||||
|
||||
# Testing Q_LENGTH type of prop
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57495"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397"
|
||||
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35"
|
||||
|
||||
When I set "active_object.PsetProperties.properties['Length'].metadata.length_value" to "0.45"
|
||||
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "450"
|
||||
|
||||
|
||||
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
@@ -24,7 +24,7 @@ Scenario: Unlink object
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material"
|
||||
And I press "bim.add_style()"
|
||||
When I press "bim.unlink_object(obj='IfcWall/Cube')"
|
||||
Then the object "Cube" is not an IFC element
|
||||
And the material "Material" is an IFC style
|
||||
|
||||
@@ -8,9 +8,9 @@ Scenario: Select all walls
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I add a new item to "scene.IfcSelectorProperties.groups"
|
||||
And I add a new item to "scene.IfcSelectorProperties.groups[0].queries"
|
||||
And I set "scene.IfcSelectorProperties.groups[0].queries[0].selector" to "IFC Class"
|
||||
And I set "scene.IfcSelectorProperties.groups[0].queries[0].active_option" to "124: IfcWall"
|
||||
When I press "bim.filter_model_elements(option='select')"
|
||||
And I press "bim.add_filter_group(module='search')"
|
||||
And I set "scene.BIMSearchProperties.facet" to "entity"
|
||||
And I press "bim.add_filter(index=0, type='entity', module='search')"
|
||||
And I set "scene.BIMSearchProperties.filter_groups[0].filters[0].value" to "IfcWall"
|
||||
When I press "bim.search"
|
||||
Then the object "IfcWall/Cube" is selected
|
||||
|
||||
@@ -18,7 +18,7 @@ Scenario: Remove style
|
||||
When I press "bim.remove_style(style={style})"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Add style
|
||||
Scenario: Add style to current blender material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
@@ -33,32 +33,39 @@ Scenario: Unlink style
|
||||
When I press "bim.unlink_style"
|
||||
Then the material "Material" is not an IFC style
|
||||
|
||||
Scenario: Enable editing style
|
||||
Scenario: Add a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
When I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
When I press "bim.add_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Scenario: Disable adding a new style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
When I press "bim.disable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
When I press "bim.disable_adding_presentation_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Edit style
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And I add a material
|
||||
And I press "bim.add_style"
|
||||
And I press "bim.enable_editing_style"
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.edit_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing style
|
||||
Given an empty IFC project
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
And I press "bim.add_presentation_style"
|
||||
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
|
||||
And I press "bim.enable_editing_style(style={style})"
|
||||
When I press "bim.disable_editing_style"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Load styles
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
|
||||
@@ -29,11 +29,12 @@ from blenderbim.bim.module.model.data import AuthoringData
|
||||
from pytest_bdd import scenarios, given, when, then, parsers
|
||||
from mathutils import Vector
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
|
||||
scenarios("feature")
|
||||
|
||||
variables = {
|
||||
"cwd": os.getcwd(),
|
||||
"cwd": Path.cwd().as_posix(),
|
||||
"ifc": "IfcStore.get_file()",
|
||||
"pset_ifc": "IfcStore.pset_template_file",
|
||||
"classification_ifc": "IfcStore.classification_file",
|
||||
@@ -44,15 +45,13 @@ webbrowser.open = lambda x: True
|
||||
|
||||
|
||||
def replace_variables(value):
|
||||
if "{cwd}" in value and os.name == "nt":
|
||||
value = value.replace("/", "\\").replace("{cwd}", os.getcwd()).replace("\\", "\\\\")
|
||||
for key, new_value in variables.items():
|
||||
value = value.replace("{" + key + "}", str(new_value))
|
||||
return value
|
||||
|
||||
|
||||
def is_x(number, x):
|
||||
return abs(number - x) < 1e-6
|
||||
return abs(number - x) < 1e-5
|
||||
|
||||
|
||||
def vectors_are_equal(v1, v2):
|
||||
@@ -177,7 +176,7 @@ def the_material_name_colour_is_set_to_colour(name, colour):
|
||||
|
||||
|
||||
@given("I add an array modifier")
|
||||
def i_add_a_cube():
|
||||
def i_add_an_array_modifier():
|
||||
bpy.ops.object.modifier_add(type="ARRAY")
|
||||
|
||||
|
||||
@@ -202,11 +201,13 @@ def i_press_operator_and_expect_error(operator, error_msg):
|
||||
else:
|
||||
exec(f"bpy.ops.{operator}()")
|
||||
assert False, f"Operator bpy.ops.{operator} ran without exception '{error_msg}'"
|
||||
except Exception as e:
|
||||
except Exception as e:
|
||||
actual_error_msg = str(e).strip()
|
||||
if str(e).strip() != error_msg:
|
||||
traceback.print_exc()
|
||||
assert False, f"Got different exception running bpy.ops.{operator} - '{actual_error_msg}' instead of '{error_msg}'"
|
||||
msg = f"Got different exception running bpy.ops.{operator} - '{actual_error_msg}' instead of '{error_msg}'"
|
||||
assert False, msg
|
||||
|
||||
|
||||
@given(parsers.parse('I press "{operator}"'))
|
||||
@when(parsers.parse('I press "{operator}"'))
|
||||
@@ -233,12 +234,14 @@ def i_evaluate_expression(expression):
|
||||
def i_duplicate_the_selected_objects():
|
||||
bpy.ops.bim.override_object_duplicate_move()
|
||||
blenderbim.bim.handler.active_object_callback()
|
||||
|
||||
|
||||
|
||||
@when("I duplicate linked aggregate the selected objects")
|
||||
def i_duplicate_the_selected_objects():
|
||||
def i_duplicate_linked_aggregate_the_selected_objects():
|
||||
bpy.ops.bim.object_duplicate_move_linked_aggregate()
|
||||
blenderbim.bim.handler.active_object_callback()
|
||||
|
||||
|
||||
@when("I refresh linked aggregate the selected object")
|
||||
def i_refresh_the_selected_objects():
|
||||
bpy.ops.bim.refresh_linked_aggregate()
|
||||
@@ -291,12 +294,18 @@ def the_object_name_is_scaled_to_scale(name, scale):
|
||||
|
||||
@given(parsers.parse('the object "{name}" is placed in the collection "{collection}"'))
|
||||
@when(parsers.parse('the object "{name}" is placed in the collection "{collection}"'))
|
||||
def the_object_name_is_placed_in_the_collection_collection(name, collection):
|
||||
def the_object_name_is_placed_in_the_collection_collection(name: str, collection: str) -> None:
|
||||
obj = the_object_name_exists(name)
|
||||
[c.objects.unlink(obj) for c in obj.users_collection]
|
||||
bpy.data.collections.get(collection).objects.link(obj)
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" is placed in the collection "{collection}"'))
|
||||
def then_the_object_name_is_placed_in_the_collection_collection(name: str, collection: str) -> None:
|
||||
obj = the_object_name_exists(name)
|
||||
assert obj in bpy.data.collections.get(collection).objects[:]
|
||||
|
||||
|
||||
@given(parsers.parse('additionally the object "{name}" is selected'))
|
||||
@when(parsers.parse('additionally the object "{name}" is selected'))
|
||||
def additionally_the_object_name_is_selected(name):
|
||||
@@ -329,7 +338,7 @@ def i_set_prop_to_value(prop, value):
|
||||
|
||||
@given(parsers.parse('I set "{prop}" to ""'))
|
||||
@when(parsers.parse('I set "{prop}" to ""'))
|
||||
def i_set_prop_to_value(prop):
|
||||
def i_set_prop_to_empty_string(prop):
|
||||
try:
|
||||
eval(f"bpy.context.{prop}")
|
||||
except:
|
||||
@@ -363,7 +372,7 @@ def nothing_happens():
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" exists'))
|
||||
def the_object_name_exists(name) -> bpy.types.Object:
|
||||
def the_object_name_exists(name: str) -> bpy.types.Object:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
assert False, f'The object "{name}" does not exist'
|
||||
@@ -377,6 +386,7 @@ def the_object_name_does_not_exist(name) -> bpy.types.Object:
|
||||
assert True, f'The object "{name}" exists'
|
||||
return obj
|
||||
|
||||
|
||||
@given(parsers.parse('the collection "{name}" exists'))
|
||||
@when(parsers.parse('the collection "{name}" exists'))
|
||||
@then(parsers.parse('the collection "{name}" exists'))
|
||||
@@ -387,6 +397,33 @@ def the_collection_name_exists(name) -> bpy.types.Collection:
|
||||
return obj
|
||||
|
||||
|
||||
@then(parsers.parse('the collection "{name}" is selectable'))
|
||||
def the_collection_name_is_selectable(name: str) -> None:
|
||||
col = the_collection_name_exists(name)
|
||||
assert col.hide_select == False
|
||||
|
||||
|
||||
@then(parsers.parse('the collection "{name}" is unselectable'))
|
||||
def the_collection_name_is_unselectable(name: str) -> None:
|
||||
col = the_collection_name_exists(name)
|
||||
assert col.hide_select == True
|
||||
|
||||
|
||||
@then(parsers.parse('the collection "{name}" exists in viewlayer'))
|
||||
def the_collection_exists_in_viewlayer(name: str) -> bpy.types.LayerCollection:
|
||||
col = the_collection_name_exists(name)
|
||||
layer = tool.Blender.get_layer_collection(col)
|
||||
if not layer:
|
||||
assert False, f'The collection "{name}" is not present in the current viewlayer'
|
||||
return layer
|
||||
|
||||
|
||||
@then(parsers.parse('the collection "{name}" exclude status is "{exclude}"'))
|
||||
def the_collection_exclude_status_is(name: str, exclude: str) -> None:
|
||||
layer = the_collection_exists_in_viewlayer(name)
|
||||
assert layer.exclude == (exclude == "True")
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name1}" and "{name2}" are different elements'))
|
||||
def the_object_name1_and_name2_are_different_elements(name1, name2):
|
||||
ifc = an_ifc_file_exists()
|
||||
@@ -413,7 +450,7 @@ def the_object_name_has_a_representation_type_of_context(name, type, context):
|
||||
|
||||
@given(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"'))
|
||||
@then(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"'))
|
||||
def the_object_name_has_a_representation_type_of_context(name, type, context):
|
||||
def the_object_name_data_is_a_type_representation_of_context(name, type, context):
|
||||
ifc = an_ifc_file_exists()
|
||||
context, subcontext, target_view = context.split("/")
|
||||
rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id)
|
||||
@@ -518,7 +555,7 @@ def the_object_name_has_no_data(name):
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" has data which is an IFC representation'))
|
||||
def the_object_name_is_not_an_ifc_element(name):
|
||||
def the_object_name_has_ifc_representation_data(name):
|
||||
id = the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id
|
||||
assert id != 0, f"The ID is {id}"
|
||||
|
||||
@@ -552,7 +589,7 @@ def the_material_name_is_not_an_ifc_style(name):
|
||||
|
||||
|
||||
@then(parsers.parse('the material "{name}" colour is "{colour}"'))
|
||||
def the_material_name_colour_is_set_to_colour(name, colour):
|
||||
def then_the_material_name_colour_is_set_to_colour(name, colour):
|
||||
diffuse_color = list(the_material_name_exists(name).diffuse_color)
|
||||
assert diffuse_color == [float(c) for c in colour.split(",")], f"The colour is {diffuse_color}"
|
||||
|
||||
@@ -616,7 +653,7 @@ def prop_is_value(prop, value):
|
||||
|
||||
|
||||
@then(parsers.parse('"{prop}" is roughly "{value}"'))
|
||||
def prop_is_value(prop, value):
|
||||
def prop_is_roughly_value(prop, value):
|
||||
prop = replace_variables(prop)
|
||||
value = replace_variables(value)
|
||||
is_value = False
|
||||
@@ -661,6 +698,7 @@ def the_object_name_has_a_thickness_thick_layered_material_containing_the_materi
|
||||
assert is_x(total_thickness, float(thickness))
|
||||
assert material_name in material_names
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" has no IFC materials'))
|
||||
def the_object_has_no_ifc_materials(name):
|
||||
element = tool.Ifc.get_entity(the_object_name_exists(name))
|
||||
@@ -705,12 +743,6 @@ def the_collection_name1_is_in_the_collection_name2(name1, name2):
|
||||
assert bpy.data.collections.get(name2).children.get(name1)
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" does not exist'))
|
||||
def the_object_name_does_not_exist(name):
|
||||
obj = bpy.data.objects.get(name)
|
||||
assert obj is None or len(obj.users_collection) == 0, f"Object {name} exists"
|
||||
|
||||
|
||||
@then(parsers.parse('the collection "{name}" does not exist'))
|
||||
def the_collection_name_does_not_exist(name):
|
||||
obj = bpy.data.collections.get(name)
|
||||
@@ -753,7 +785,7 @@ def the_object_name_dimensions_are_dimensions(name, dimensions):
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" top right corner is at "{location}"'))
|
||||
def the_object_name_is_at_location(name, location):
|
||||
def the_object_name_top_right_corner_is_at_location(name, location):
|
||||
obj = the_object_name_exists(name)
|
||||
obj_corner = obj.matrix_world @ Vector(obj.bound_box[6])
|
||||
assert (
|
||||
@@ -762,7 +794,7 @@ def the_object_name_is_at_location(name, location):
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" bottom left corner is at "{location}"'))
|
||||
def the_object_name_is_at_location(name, location):
|
||||
def the_object_name_bottom_left_corner_is_at_location(name, location):
|
||||
obj = the_object_name_exists(name)
|
||||
obj_corner = obj.matrix_world @ Vector(obj.bound_box[0])
|
||||
assert (
|
||||
@@ -791,11 +823,13 @@ def the_file_name_should_contain_value(name, value):
|
||||
def the_object_name_has_no_modifiers(name):
|
||||
assert len(the_object_name_exists(name).modifiers) == 0
|
||||
|
||||
|
||||
@given(parsers.parse('I load the IFC test file "{filepath}"'))
|
||||
def i_load_the_ifc_test_file(filepath):
|
||||
filepath = f"{variables['cwd']}{filepath}"
|
||||
bpy.ops.bim.load_project(filepath=filepath, use_relative_path=True)
|
||||
|
||||
|
||||
@given("I load the demo construction library")
|
||||
@when("I load the demo construction library")
|
||||
def i_add_a_construction_library():
|
||||
@@ -836,7 +870,7 @@ def move_cursor_bottom_left():
|
||||
@given(parsers.parse("I prepare to undo"))
|
||||
@when(parsers.parse("I prepare to undo"))
|
||||
@then(parsers.parse("I prepare to undo"))
|
||||
def hit_undo():
|
||||
def prepare_undo():
|
||||
bpy.ops.ed.undo_push(message="UNDO STEP")
|
||||
|
||||
|
||||
@@ -847,6 +881,7 @@ def hit_undo():
|
||||
bpy.ops.ed.undo_push(message="UNDO STEP")
|
||||
bpy.ops.ed.undo()
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"'))
|
||||
def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2):
|
||||
element1 = replace_variables(obj_name1)
|
||||
@@ -866,9 +901,11 @@ def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2):
|
||||
relationships[conn.RelatedElement] = conn.RelatingElement
|
||||
|
||||
for key, value in relationships.items():
|
||||
assert (key.id() == element1.id() and value.id() == element2.id()) or (key.id() == element2.id() and value.id() == element1.id()), f"The object {obj_name1} is connected to {obj_name2}"
|
||||
assert (key.id() == element1.id() and value.id() == element2.id()) or (
|
||||
key.id() == element2.id() and value.id() == element1.id()
|
||||
), f"The object {obj_name1} is connected to {obj_name2}"
|
||||
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{obj_name1}" and "{obj_name2}" belong to the same Linked Aggregate Group'))
|
||||
def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name2):
|
||||
element1 = replace_variables(obj_name1)
|
||||
@@ -892,6 +929,7 @@ def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name
|
||||
|
||||
assert groups[0] == groups[1], "Objects do not belong to the same Linked Aggregate group"
|
||||
|
||||
|
||||
@when(parsers.parse('the object layer length is set to "{value}"'))
|
||||
def the_obj_layer_lenght_is_set_to(value):
|
||||
value = float(value)
|
||||
@@ -900,11 +938,11 @@ def the_obj_layer_lenght_is_set_to(value):
|
||||
except:
|
||||
assert False, f"Property BIMModelProperties.length does not exist when trying to set to value {value}"
|
||||
|
||||
print(50*"@", bpy.context.selected_objects)
|
||||
|
||||
print(50 * "@", bpy.context.selected_objects)
|
||||
|
||||
bpy.context.scene.BIMModelProperties.length = value
|
||||
bpy.ops.bim.change_layer_length(length=value)
|
||||
|
||||
|
||||
|
||||
# These definitions are not to be used in tests but simply in debugging failing tests
|
||||
|
||||
@@ -953,5 +991,3 @@ def run_pdb():
|
||||
import pdb
|
||||
|
||||
pdb.set_trace()
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import datetime
|
||||
from datetime import timedelta, date
|
||||
from typing import List
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.date
|
||||
@@ -25,7 +26,7 @@ import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class MSP2Ifc:
|
||||
def __init__(self):
|
||||
def __init__(self, optionalColumns: List[str] = []):
|
||||
self.xml = None
|
||||
self.file = None
|
||||
self.ns = None
|
||||
@@ -33,6 +34,7 @@ class MSP2Ifc:
|
||||
self.project = {}
|
||||
self.calendars = {}
|
||||
self.tasks = {}
|
||||
self.optionalColumns = optionalColumns
|
||||
self.resources = {}
|
||||
self.RESOURCE_TYPES_MAPPING = {"1": "LABOR", "0": "MATERIAL", "2": None}
|
||||
|
||||
@@ -106,6 +108,18 @@ class MSP2Ifc:
|
||||
"ifc": None,
|
||||
}
|
||||
|
||||
# retrieve optional columns
|
||||
# If first column = "all" then retrieve all columns
|
||||
if len(self.optionalColumns) and self.optionalColumns[0] == "all":
|
||||
self.optionalColumns = [child.tag.split("}")[1] for child in task]
|
||||
|
||||
for column in self.optionalColumns:
|
||||
if not self.tasks[task_id].get(column):
|
||||
self.tasks[task_id][column] = task.find(f"pr:{column}", self.ns).text if task.find(f"pr:{column}", self.ns) else None
|
||||
|
||||
|
||||
|
||||
|
||||
def parse_calendar_xml(self, project):
|
||||
def parse_working_times(day):
|
||||
working_times = []
|
||||
@@ -200,7 +214,8 @@ class MSP2Ifc:
|
||||
def create_tasks(self, work_schedule):
|
||||
for task_id in self.tasks:
|
||||
task = self.tasks[task_id]
|
||||
if task["OutlineLevel"] == 0:
|
||||
# Outline Level can be None or 0
|
||||
if not task["OutlineLevel"]:
|
||||
self.create_task(task, work_schedule=work_schedule)
|
||||
|
||||
def create_work_schedule(self):
|
||||
@@ -268,6 +283,18 @@ class MSP2Ifc:
|
||||
for subtask_id in task["subtasks"]:
|
||||
self.create_task(self.tasks[subtask_id], parent_task=task)
|
||||
|
||||
|
||||
# create pset for optional columns
|
||||
if len(self.optionalColumns):
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=task["ifc"] , name="Pset_MSP_Task")
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
self.file,
|
||||
pset=pset,
|
||||
properties= {name: str(task[name]) for name in self.optionalColumns if task[name]}
|
||||
)
|
||||
|
||||
def process_working_week(self, week, calendar):
|
||||
day_map = {
|
||||
"1": 7, # Sunday
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# IfcClash - IFC-based clash detection.
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcClash.
|
||||
#
|
||||
# IfcClash is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcClash is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hppfcl
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
|
||||
|
||||
class Collider:
|
||||
def __init__(self, logger):
|
||||
self.logger = logger
|
||||
self.groups = {}
|
||||
self.tree = ifcopenshell.geom.tree()
|
||||
|
||||
def create_group(self, name):
|
||||
self.logger.info(f"Creating group {name}")
|
||||
self.groups[name] = {"elements": {}, "objects": {}}
|
||||
|
||||
def create_objects(self, name, ifc_file, iterator, elements):
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
self.logger.info(f"Adding objects {name}")
|
||||
assert iterator.initialize()
|
||||
while True:
|
||||
self.tree.add_element(iterator.get_native())
|
||||
shape = iterator.get()
|
||||
self.create_object(name, shape.guid, shape)
|
||||
if not iterator.next():
|
||||
break
|
||||
self.logger.info(f"Tree finished {time.time() - start}")
|
||||
start = time.time()
|
||||
self.groups[name]["elements"].update({e.GlobalId: e for e in elements})
|
||||
self.logger.info(f"Element metadata finished {time.time() - start}")
|
||||
start = time.time()
|
||||
|
||||
def create_object(self, group_name, id, shape):
|
||||
obj = hppfcl.CollisionObject(
|
||||
self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data)
|
||||
)
|
||||
self.groups[group_name]["objects"][id] = obj
|
||||
|
||||
def collide_internal(self, name):
|
||||
return self.collide_narrowphase(name, name, self.collide_broadphase(name, name))
|
||||
|
||||
def collide_group(self, name1, name2):
|
||||
return self.collide_narrowphase(name1, name2, self.collide_broadphase(name1, name2))
|
||||
|
||||
def collide_broadphase(self, name1, name2):
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
self.logger.info("Starting broadphase")
|
||||
potential_collisions = []
|
||||
checked_collisions = set()
|
||||
for id, element in self.groups[name1]["elements"].items():
|
||||
checked_collisions.add(id)
|
||||
box_filter = self.tree.select_box(element)
|
||||
pairs = [
|
||||
{"id1": id, "id2": e.GlobalId}
|
||||
for e in box_filter
|
||||
if e.GlobalId not in checked_collisions and e.GlobalId in self.groups[name2]["elements"]
|
||||
]
|
||||
potential_collisions.extend(pairs)
|
||||
self.logger.info(f"Finished broadphase {time.time() - start}")
|
||||
return potential_collisions
|
||||
|
||||
def collide_narrowphase(self, name1, name2, potential_collisions):
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
self.logger.info("Starting narrowphase")
|
||||
collisions = []
|
||||
for data in potential_collisions:
|
||||
result = hppfcl.CollisionResult()
|
||||
hppfcl.collide(
|
||||
self.groups[name1]["objects"][data["id1"]],
|
||||
self.groups[name2]["objects"][data["id2"]],
|
||||
hppfcl.CollisionRequest(),
|
||||
result,
|
||||
)
|
||||
if result.isCollision():
|
||||
collisions.append({"id1": data["id1"], "id2": data["id2"], "collision": result})
|
||||
self.logger.info(f"Finished narrowphase {time.time() - start}")
|
||||
return collisions
|
||||
|
||||
def create_transform(self, m):
|
||||
mat = np.array([[m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]])
|
||||
mat.transpose()
|
||||
return hppfcl.Transform3f(mat[:3, :3], mat[:3, 3])
|
||||
|
||||
def create_bvh(self, mesh):
|
||||
v = mesh.verts
|
||||
f = mesh.faces
|
||||
mesh_verts = np.array([[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)])
|
||||
mesh_faces = [(int(f[i]), int(f[i + 1]), int(f[i + 2])) for i in range(0, len(f), 3)]
|
||||
|
||||
bvh = hppfcl.BVHModelOBB()
|
||||
bvh.beginModel(len(mesh_faces), len(mesh_verts))
|
||||
vertices = hppfcl.StdVec_Vec3f()
|
||||
[vertices.append(v) for v in mesh_verts]
|
||||
triangles = hppfcl.StdVec_Triangle()
|
||||
[triangles.append(hppfcl.Triangle(f[0], f[1], f[2])) for f in mesh_faces]
|
||||
bvh.addSubModel(vertices, triangles)
|
||||
bvh.endModel()
|
||||
return bvh
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# IfcClash - IFC-based clash detection.
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020-2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcClash.
|
||||
#
|
||||
@@ -19,14 +19,13 @@
|
||||
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import numpy as np
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.selector
|
||||
from . import collider
|
||||
|
||||
|
||||
class Clasher:
|
||||
@@ -34,55 +33,78 @@ class Clasher:
|
||||
self.settings = settings
|
||||
self.geom_settings = ifcopenshell.geom.settings()
|
||||
self.clash_sets = []
|
||||
self.collider = collider.Collider(self.settings.logger)
|
||||
self.selector = ifcopenshell.util.selector.Selector()
|
||||
self.logger = self.settings.logger
|
||||
self.groups = {}
|
||||
self.ifcs = {}
|
||||
self.tree = None
|
||||
|
||||
def clash(self):
|
||||
existing_limit = sys.getrecursionlimit()
|
||||
for clash_set in self.clash_sets:
|
||||
self.process_clash_set(clash_set)
|
||||
|
||||
def process_clash_set(self, clash_set):
|
||||
self.collider.create_group("a")
|
||||
self.tree = ifcopenshell.geom.tree()
|
||||
self.create_group("a")
|
||||
for source in clash_set["a"]:
|
||||
source["ifc"] = self.load_ifc(source["file"])
|
||||
self.add_collision_objects("a", source["ifc"], source.get("mode", None), source.get("selector", None))
|
||||
|
||||
if "b" in clash_set:
|
||||
self.collider.create_group("b")
|
||||
if "b" in clash_set and clash_set["b"]:
|
||||
self.create_group("b")
|
||||
for source in clash_set["b"]:
|
||||
source["ifc"] = self.load_ifc(source["file"])
|
||||
self.add_collision_objects("b", source["ifc"], source.get("mode", None), source.get("selector", None))
|
||||
results = self.collider.collide_group("a", "b")
|
||||
b = "b"
|
||||
else:
|
||||
results = self.collider.collide_internal("a")
|
||||
b = "a"
|
||||
|
||||
mode = clash_set["mode"]
|
||||
if mode == "intersection":
|
||||
results = self.tree.clash_intersection_many(
|
||||
list(self.groups["a"]["elements"].values()),
|
||||
list(self.groups[b]["elements"].values()),
|
||||
tolerance=clash_set["tolerance"],
|
||||
check_all=clash_set["check_all"],
|
||||
)
|
||||
elif mode == "collision":
|
||||
results = self.tree.clash_collision_many(
|
||||
list(self.groups["a"]["elements"].values()),
|
||||
list(self.groups[b]["elements"].values()),
|
||||
allow_touching=clash_set["allow_touching"],
|
||||
)
|
||||
elif mode == "clearance":
|
||||
results = self.tree.clash_clearance_many(
|
||||
list(self.groups["a"]["elements"].values()),
|
||||
list(self.groups[b]["elements"].values()),
|
||||
clearance=clash_set["clearance"],
|
||||
check_all=clash_set["check_all"],
|
||||
)
|
||||
|
||||
processed_results = {}
|
||||
for result in results:
|
||||
element1 = self.get_element(clash_set["a"], result["id1"])
|
||||
if "b" in clash_set:
|
||||
element2 = self.get_element(clash_set["b"], result["id2"])
|
||||
else:
|
||||
element2 = self.get_element(clash_set["a"], result["id2"])
|
||||
element1 = result.a
|
||||
element2 = result.b
|
||||
|
||||
contact = result["collision"].getContacts()[0]
|
||||
processed_results[f"{result['id1']}-{result['id2']}"] = {
|
||||
"a_global_id": result["id1"],
|
||||
"b_global_id": result["id2"],
|
||||
processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = {
|
||||
"a_global_id": element1.get_argument(0),
|
||||
"b_global_id": element2.get_argument(0),
|
||||
"a_ifc_class": element1.is_a(),
|
||||
"b_ifc_class": element2.is_a(),
|
||||
"a_name": element1.Name,
|
||||
"b_name": element2.Name,
|
||||
"normal": list(contact.normal),
|
||||
"position": list(contact.pos),
|
||||
"penetration_depth": contact.penetration_depth,
|
||||
"a_name": element1.get_argument(2),
|
||||
"b_name": element2.get_argument(2),
|
||||
"type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type],
|
||||
"p1": list(result.p1),
|
||||
"p2": list(result.p2),
|
||||
"distance": result.distance,
|
||||
}
|
||||
clash_set["clashes"] = processed_results
|
||||
self.logger.info(f"Found clashes: {len(processed_results.keys())}")
|
||||
|
||||
def create_group(self, name):
|
||||
self.logger.info(f"Creating group {name}")
|
||||
self.groups[name] = {"elements": {}, "objects": {}}
|
||||
|
||||
def load_ifc(self, path):
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
self.settings.logger.info(f"Loading IFC {path}")
|
||||
ifc = self.ifcs.get(path, None)
|
||||
@@ -93,21 +115,35 @@ class Clasher:
|
||||
return ifc
|
||||
|
||||
def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
self.settings.logger.info("Creating iterator")
|
||||
if not mode:
|
||||
elements = ifc_file.by_type("IfcElement")
|
||||
if not mode or mode == "a" or not selector:
|
||||
elements = set(ifc_file.by_type("IfcElement"))
|
||||
elements -= set(ifc_file.by_type("IfcFeatureElement"))
|
||||
elif mode == "e":
|
||||
elements = set(ifc_file.by_type("IfcElement")) - set(self.selector.parse(ifc_file, selector))
|
||||
elements = set(ifc_file.by_type("IfcElement"))
|
||||
elements -= set(ifc_file.by_type("IfcFeatureElement"))
|
||||
elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, selector))
|
||||
elif mode == "i":
|
||||
elements = self.selector.parse(ifc_file, selector)
|
||||
elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, selector))
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements
|
||||
)
|
||||
self.settings.logger.info(f"Iterator creation finished {time.time() - start}")
|
||||
self.collider.create_objects(name, ifc_file, iterator, elements)
|
||||
|
||||
start = time.time()
|
||||
self.logger.info(f"Adding objects {name}")
|
||||
assert iterator.initialize()
|
||||
while True:
|
||||
self.tree.add_element(iterator.get())
|
||||
shape = iterator.get()
|
||||
if not iterator.next():
|
||||
break
|
||||
self.logger.info(f"Tree finished {time.time() - start}")
|
||||
start = time.time()
|
||||
self.groups[name]["elements"].update({e.GlobalId: e for e in elements})
|
||||
self.logger.info(f"Element metadata finished {time.time() - start}")
|
||||
start = time.time()
|
||||
|
||||
def export(self):
|
||||
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
|
||||
@@ -123,7 +159,9 @@ class Clasher:
|
||||
title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}'
|
||||
topic = bcfxml.add_topic(title, title, "IfcClash")
|
||||
viewpoint = topic.add_viewpoint_from_point_and_guids(
|
||||
np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"],
|
||||
np.array(clash["position"]),
|
||||
clash["a_global_id"],
|
||||
clash["b_global_id"],
|
||||
)
|
||||
snapshot = self.get_viewpoint_snapshot(viewpoint)
|
||||
if snapshot:
|
||||
@@ -147,15 +185,6 @@ class Clasher:
|
||||
with open(self.settings.output, "w", encoding="utf-8") as clashes_file:
|
||||
json.dump(clash_sets, clashes_file, indent=4)
|
||||
|
||||
def get_element(self, clash_set, global_id):
|
||||
for source in clash_set:
|
||||
try:
|
||||
element = source["ifc"].by_guid(global_id)
|
||||
if element:
|
||||
return element
|
||||
except:
|
||||
pass
|
||||
|
||||
def smart_group_clashes(self, clash_sets, max_clustering_distance):
|
||||
from sklearn.cluster import OPTICS
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -240,13 +240,19 @@ class IfcCsv:
|
||||
index = attributes.index(data["name"])
|
||||
formatting_indices[index] = data["format"]
|
||||
|
||||
for row in self.results:
|
||||
for index, format_query in formatting_indices.items():
|
||||
for index, format_query in formatting_indices.items():
|
||||
for row in self.results:
|
||||
if row[index] == null:
|
||||
continue
|
||||
row[index] = '"' + str(row[index]).replace('"', '\\"') + '"'
|
||||
row[index] = ifcopenshell.util.selector.format(format_query.replace("{{value}}", row[index]))
|
||||
|
||||
if self.summaries[index] is not None:
|
||||
summary_label, summary_value = self.summaries[index].split(": ")
|
||||
summary_value = '"' + str(summary_value).replace('"', '\\"') + '"'
|
||||
summary_value = ifcopenshell.util.selector.format(format_query.replace("{{value}}", summary_value))
|
||||
self.summaries[index] = summary_label + ": " + str(summary_value)
|
||||
|
||||
def sort_results(self, sort, attributes, include_global_id):
|
||||
if not self.results:
|
||||
return
|
||||
|
||||
@@ -54,7 +54,7 @@ class IfcDiff:
|
||||
that comparisons will take longer.
|
||||
:type is_shallow: bool
|
||||
:param filter_elements: An IFC filter query if you only want to compare a
|
||||
subset of elements. For example: ``.IfcWall`` to only compare walls.
|
||||
subset of elements. For example: ``IfcWall`` to only compare walls.
|
||||
:type filter_elements: string
|
||||
|
||||
Example::
|
||||
@@ -83,9 +83,12 @@ class IfcDiff:
|
||||
self.precision = self.get_precision()
|
||||
|
||||
if self.filter_elements:
|
||||
selector = ifcopenshell.util.selector.Selector()
|
||||
old_elements = set(e.GlobalId for e in selector.parse(self.old, self.filter_elements))
|
||||
new_elements = set(e.GlobalId for e in selector.parse(self.new, self.filter_elements))
|
||||
old_elements = set(
|
||||
e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.old, self.filter_elements)
|
||||
)
|
||||
new_elements = set(
|
||||
e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.new, self.filter_elements)
|
||||
)
|
||||
else:
|
||||
old_elements = self.old.by_type("IfcElement")
|
||||
if self.old.schema == "IFC2X3":
|
||||
|
||||
@@ -28,7 +28,19 @@
|
||||
bool IfcGeom::Kernel::convert(const IfcSchema::IfcDerivedProfileDef* l, TopoDS_Shape& face) {
|
||||
TopoDS_Face f;
|
||||
gp_Trsf2d trsf2d;
|
||||
if (convert_face(l->ParentProfile(), f) && IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
bool is_mirror = false;
|
||||
#ifdef SCHEMA_HAS_IfcMirroredProfileDef
|
||||
if (l->as<IfcSchema::IfcMirroredProfileDef>()) {
|
||||
trsf2d.SetMirror(gp::Origin2d());
|
||||
is_mirror = true;
|
||||
}
|
||||
#endif
|
||||
if (!is_mirror) {
|
||||
if (!IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (convert_face(l->ParentProfile(), f)) {
|
||||
gp_Trsf trsf = trsf2d;
|
||||
face = BRepBuilderAPI_Transform(f, trsf).Shape();
|
||||
return true;
|
||||
|
||||
@@ -859,7 +859,9 @@ IfcSchema::IfcRepresentation* IfcGeom::Kernel::representation_mapped_to(const If
|
||||
}
|
||||
}
|
||||
} catch (const IfcParse::IfcException& e) {
|
||||
Logger::Error(e);
|
||||
// @todo the remove_boundingboxes() in IfcConvert leave dangling IfcRepresentationMap
|
||||
// causing this error to be logged.
|
||||
Logger::Error(e, representation);
|
||||
// @todo reset representation_mapped_to to zero?
|
||||
}
|
||||
return representation_mapped_to;
|
||||
|
||||
@@ -371,6 +371,11 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
|
||||
triangles(i).Get(n3, n2, n1);
|
||||
else triangles(i).Get(n1, n2, n3);
|
||||
|
||||
if (dict[n1] == dict[n2] || dict[n2] == dict[n3] || dict[n3] == dict[n1]) {
|
||||
Logger::Warning("Mesher generated a degenerate triangle, ignoring");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* An alternative would be to calculate normals based
|
||||
* on the coordinates of the mesh vertices */
|
||||
/*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,8 @@
|
||||
#include <ShapeFix_Shape.hxx>
|
||||
#include <BRepCheck_Analyzer.hxx>
|
||||
#include <BRepClass3d_SolidClassifier.hxx>
|
||||
#include <BRepCheck.hxx>
|
||||
#include <BRepTools.hxx>
|
||||
|
||||
#include <TopTools_IndexedMapOfShape.hxx>
|
||||
|
||||
@@ -739,3 +741,43 @@ bool IfcGeom::util::create_solid_from_faces(const TopTools_ListOfShape& face_lis
|
||||
|
||||
return valid_shell;
|
||||
}
|
||||
|
||||
bool IfcGeom::util::validate_shape(const TopoDS_Shape& s) {
|
||||
BRepCheck_Analyzer ana(s);
|
||||
if (ana.IsValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::stringstream str;
|
||||
bool any_emitted = false;
|
||||
|
||||
std::function<void(const TopoDS_Shape&)> dump;
|
||||
dump = [&ana, &str, &dump, &any_emitted](const TopoDS_Shape& s) {
|
||||
if (!ana.Result(s).IsNull()) {
|
||||
BRepCheck_ListIteratorOfListOfStatus itl;
|
||||
itl.Initialize(ana.Result(s)->Status());
|
||||
for (; itl.More(); itl.Next()) {
|
||||
if (itl.Value() != BRepCheck_NoError) {
|
||||
if (any_emitted) {
|
||||
str << ", ";
|
||||
}
|
||||
BRepCheck::Print(itl.Value(), str);
|
||||
str.seekp(str.tellp() - (std::streamoff)1);
|
||||
str << " on ";
|
||||
TopAbs::Print(s.ShapeType(), str);
|
||||
BRepTools::Dump(s, str);
|
||||
any_emitted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (TopoDS_Iterator it(s); it.More(); it.Next()) {
|
||||
dump(it.Value());
|
||||
}
|
||||
};
|
||||
|
||||
dump(s);
|
||||
|
||||
Logger::Warning(str.str());
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -70,6 +70,8 @@ namespace IfcGeom {
|
||||
|
||||
TopoDS_Shape apply_transformation(const TopoDS_Shape&, const gp_Trsf&);
|
||||
TopoDS_Shape apply_transformation(const TopoDS_Shape&, const gp_GTrsf&);
|
||||
|
||||
bool validate_shape(const TopoDS_Shape&);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
#include "clash_utils.h"
|
||||
#include <cassert>
|
||||
|
||||
#define GU_CULLING_EPSILON_RAY_TRIANGLE FLT_EPSILON*FLT_EPSILON
|
||||
#define PX_MAX_F32 3.4028234663852885981170418348452e+38F
|
||||
|
||||
typedef uint32_t PxU32;
|
||||
|
||||
// Why can't I use std::clamp?
|
||||
template<typename TC>
|
||||
const TC& ios_clamp(const TC& v, const TC& lo, const TC& hi) {
|
||||
assert(!(hi < lo));
|
||||
return (v < lo) ? lo : (hi < v) ? hi : v;
|
||||
}
|
||||
|
||||
// Branchless slab method. Note that this can still be optimised further by batching boxes.
|
||||
// From Tavian Barnes - MIT License
|
||||
// https://tavianator.com/2022/ray_box_boundary.html
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
|
||||
float tmin = 0.0, tmax = INFINITY;
|
||||
|
||||
for (int d = 0; d < 3; ++d) {
|
||||
bool sign = std::signbit(ray->dir_inv[d]);
|
||||
float bmin = box->corners[sign][d];
|
||||
float bmax = box->corners[!sign][d];
|
||||
|
||||
float dmin = (bmin - ray->origin[d]) * ray->dir_inv[d];
|
||||
float dmax = (bmax - ray->origin[d]) * ray->dir_inv[d];
|
||||
|
||||
tmin = std::max(dmin, tmin);
|
||||
tmax = std::min(dmax, tmax);
|
||||
}
|
||||
|
||||
return tmin < tmax;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge) {
|
||||
// Find vectors for two edges sharing vert0
|
||||
const gp_Vec edge1 = vert1 - vert0;
|
||||
const gp_Vec edge2 = vert2 - vert0;
|
||||
|
||||
// Begin calculating determinant - also used to calculate U parameter
|
||||
const gp_Vec pvec = dir.Crossed(edge2); // error ~ |v2-v0|
|
||||
|
||||
// If determinant is near zero, ray lies in plane of triangle
|
||||
const Standard_Real det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0|
|
||||
|
||||
if(cull)
|
||||
{
|
||||
if(det<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0;
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec);
|
||||
|
||||
const Standard_Real enlargeCoeff = enlarge*det;
|
||||
const Standard_Real uvlimit = -enlargeCoeff;
|
||||
const Standard_Real uvlimit2 = det + enlargeCoeff;
|
||||
|
||||
if(u<uvlimit || u>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec);
|
||||
if(v<uvlimit || (u+v)>uvlimit2)
|
||||
return false;
|
||||
|
||||
// Calculate t, scale parameters, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec);
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
at = t*inv_det;
|
||||
au = u*inv_det;
|
||||
av = v*inv_det;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the non-culling branch
|
||||
if(std::abs(det)<GU_CULLING_EPSILON_RAY_TRIANGLE)
|
||||
return false;
|
||||
|
||||
const Standard_Real inv_det = 1.0f / det;
|
||||
|
||||
// Calculate distance from vert0 to ray origin
|
||||
const gp_Vec tvec = orig - vert0; // error ~ |orig-v0|
|
||||
|
||||
// Calculate U parameter and test bounds
|
||||
const Standard_Real u = tvec.Dot(pvec) * inv_det;
|
||||
if(u<-enlarge || u>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// prepare to test V parameter
|
||||
const gp_Vec qvec = tvec.Crossed(edge1);
|
||||
|
||||
// Calculate V parameter and test bounds
|
||||
const Standard_Real v = dir.Dot(qvec) * inv_det;
|
||||
if(v<-enlarge || (u+v)>1.0f+enlarge)
|
||||
return false;
|
||||
|
||||
// Calculate t, ray intersects triangle
|
||||
const Standard_Real t = edge2.Dot(qvec) * inv_det;
|
||||
|
||||
at = t;
|
||||
au = u;
|
||||
av = v;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/sweep/GuSweepCapsuleCapsule.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b) // seg 2 origin, vector
|
||||
{
|
||||
const gp_Vec Tx = q - p;
|
||||
const double ADotA = a.Dot(a);
|
||||
const double BDotB = b.Dot(b);
|
||||
const double ADotB = a.Dot(b);
|
||||
const double ADotT = a.Dot(Tx);
|
||||
const double BDotT = b.Dot(Tx);
|
||||
|
||||
// t parameterizes ray (p, a)
|
||||
// u parameterizes ray (q, b)
|
||||
|
||||
// Compute t for the closest point on ray (p, a) to ray (q, b)
|
||||
const Standard_Real Denom = ADotA*BDotB - ADotB*ADotB;
|
||||
|
||||
Standard_Real t; // We will clamp result so t is on the segment (p, a)
|
||||
if(Denom!=0.0f)
|
||||
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
|
||||
// find u for point on ray (q, b) closest to point at t
|
||||
Standard_Real u;
|
||||
if(BDotB!=0.0f)
|
||||
{
|
||||
u = (t*ADotB - BDotT) / BDotB;
|
||||
|
||||
// if u is on segment (q, b), t and u correspond to closest points, otherwise, clamp u, recompute and clamp t
|
||||
if(u<0.0f)
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
else if(u > 1.0f)
|
||||
{
|
||||
u = 1.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp((ADotB + ADotT) / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
u = 0.0f;
|
||||
if(ADotA!=0.0f)
|
||||
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0f;
|
||||
}
|
||||
|
||||
x = p + a * t;
|
||||
y = q + b * u;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/distance/GuDistanceTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q)
|
||||
{
|
||||
std::array<gp_Vec, 3> Sv;
|
||||
Sv[0] = p[1] - p[0];
|
||||
Sv[1] = p[2] - p[1];
|
||||
Sv[2] = p[0] - p[2];
|
||||
|
||||
std::array<gp_Vec, 3> Tv;
|
||||
Tv[0] = q[1] - q[0];
|
||||
Tv[1] = q[2] - q[1];
|
||||
Tv[2] = q[0] - q[2];
|
||||
|
||||
gp_Vec minP, minQ;
|
||||
bool shown_disjoint = false;
|
||||
|
||||
float mindd = PX_MAX_F32;
|
||||
|
||||
for(int i=0;i<3;i++)
|
||||
{
|
||||
for(int j=0;j<3;j++)
|
||||
{
|
||||
edgeEdgeDist(cp, cq, p[i], Sv[i], q[j], Tv[j]);
|
||||
const gp_Vec V = cq - cp;
|
||||
const float dd = V.Dot(V);
|
||||
|
||||
if(dd<=mindd)
|
||||
{
|
||||
minP = cp;
|
||||
minQ = cq;
|
||||
mindd = dd;
|
||||
|
||||
int id = i+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
gp_Vec Z = p[id] - cp;
|
||||
float a = Z.Dot(V);
|
||||
id = j+2;
|
||||
if(id>=3)
|
||||
id-=3;
|
||||
Z = q[id] - cq;
|
||||
float b = Z.Dot(V);
|
||||
|
||||
if((a<=0.0f) && (b>=0.0f))
|
||||
return V.Dot(V);
|
||||
|
||||
if(a<=0.0f) a = 0.0f;
|
||||
else if(b>0.0f) b = 0.0f;
|
||||
|
||||
if((mindd - a + b) > 0.0f)
|
||||
shown_disjoint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Sn = Sv[0].Crossed(Sv[1]);
|
||||
float Snl = Sn.Dot(Sn);
|
||||
|
||||
if(Snl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Tp = {(p[0] - q[0]).Dot(Sn),
|
||||
(p[0] - q[1]).Dot(Sn),
|
||||
(p[0] - q[2]).Dot(Sn)};
|
||||
|
||||
int index = -1;
|
||||
if((Tp[0]>0.0f) && (Tp[1]>0.0f) && (Tp[2]>0.0f))
|
||||
{
|
||||
if(Tp[0]<Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]<Tp[index]) index = 2;
|
||||
}
|
||||
else if((Tp[0]<0.0f) && (Tp[1]<0.0f) && (Tp[2]<0.0f))
|
||||
{
|
||||
if(Tp[0]>Tp[1]) index = 0; else index = 1;
|
||||
if(Tp[2]>Tp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& qIndex = q[index];
|
||||
|
||||
gp_Vec V = qIndex - p[0];
|
||||
gp_Vec Z = Sn.Crossed(Sv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[1];
|
||||
Z = Sn.Crossed(Sv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = qIndex - p[2];
|
||||
Z = Sn.Crossed(Sv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = qIndex + Sn * Tp[index]/Snl;
|
||||
cq = qIndex;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
|
||||
float Tnl = Tn.Dot(Tn);
|
||||
|
||||
if(Tnl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Sp = {(q[0] - p[0]).Dot(Tn),
|
||||
(q[0] - p[1]).Dot(Tn),
|
||||
(q[0] - p[2]).Dot(Tn)};
|
||||
|
||||
int index = -1;
|
||||
if((Sp[0]>0.0f) && (Sp[1]>0.0f) && (Sp[2]>0.0f))
|
||||
{
|
||||
if(Sp[0]<Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]<Sp[index]) index = 2;
|
||||
}
|
||||
else if((Sp[0]<0.0f) && (Sp[1]<0.0f) && (Sp[2]<0.0f))
|
||||
{
|
||||
if(Sp[0]>Sp[1]) index = 0; else index = 1;
|
||||
if(Sp[2]>Sp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& pIndex = p[index];
|
||||
|
||||
gp_Vec V = pIndex - q[0];
|
||||
gp_Vec Z = Tn.Crossed(Tv[0]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[1];
|
||||
Z = Tn.Crossed(Tv[1]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
V = pIndex - q[2];
|
||||
Z = Tn.Crossed(Tv[2]);
|
||||
if(V.Dot(Z)>0.0f)
|
||||
{
|
||||
cp = pIndex;
|
||||
cq = pIndex + Tn * Sp[index]/Tnl;
|
||||
return (cp - cq).SquareMagnitude();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(shown_disjoint)
|
||||
{
|
||||
cp = minP;
|
||||
cq = minQ;
|
||||
return mindd;
|
||||
}
|
||||
else return 0.0f;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
//Based on the paper A Fast Triangle-Triangle Intersection Test by T. Moeller
|
||||
//http://web.stanford.edu/class/cs277/resources/papers/Moller1997b.pdf
|
||||
namespace {
|
||||
struct Interval
|
||||
{
|
||||
Standard_Real min;
|
||||
Standard_Real max;
|
||||
gp_Vec minPoint;
|
||||
gp_Vec maxPoint;
|
||||
|
||||
Interval() : min(FLT_MAX), max(-FLT_MAX), minPoint(gp_Vec(NAN, NAN, NAN)), maxPoint(gp_Vec(NAN, NAN, NAN)) { }
|
||||
|
||||
static bool overlapOrTouch(const Interval& a, const Interval& b)
|
||||
{
|
||||
return !(a.min > b.max || b.min > a.max);
|
||||
}
|
||||
|
||||
static Interval intersection(const Interval& a, const Interval& b)
|
||||
{
|
||||
Interval result;
|
||||
if (!overlapOrTouch(a, b))
|
||||
return result;
|
||||
|
||||
if (a.min > b.min) {
|
||||
result.min = a.min;
|
||||
result.minPoint = a.minPoint;
|
||||
} else {
|
||||
result.min = b.min;
|
||||
result.minPoint = b.minPoint;
|
||||
}
|
||||
|
||||
if (a.max < b.max) {
|
||||
result.max = a.max;
|
||||
result.maxPoint = a.maxPoint;
|
||||
} else {
|
||||
result.max = b.max;
|
||||
result.maxPoint = b.maxPoint;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void include(Standard_Real d, const gp_Vec& p)
|
||||
{
|
||||
if (d < min) { min = d; minPoint = p; }
|
||||
if (d > max) { max = d; maxPoint = p; }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB, Standard_Real distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir)
|
||||
{
|
||||
Interval i;
|
||||
|
||||
const bool bA = distanceA > 0;
|
||||
const bool bB = distanceB > 0;
|
||||
const bool bC = distanceC > 0;
|
||||
distanceA = std::abs(distanceA);
|
||||
distanceB = std::abs(distanceB);
|
||||
distanceC = std::abs(distanceC);
|
||||
|
||||
if (bA != bB)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceB)) * b + (distanceB / (distanceA + distanceB)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bA != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceA / (distanceA + distanceC)) * c + (distanceC / (distanceA + distanceC)) * a;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
if (bB != bC)
|
||||
{
|
||||
const gp_Vec p = (distanceB / (distanceB + distanceC)) * c + (distanceC / (distanceB + distanceC)) * b;
|
||||
i.include(dir.Dot(p), p);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y)
|
||||
{
|
||||
return (a.Coord(y) - c.Coord(y)) * (b.Coord(x) - c.Coord(x)) - (a.Coord(x) - c.Coord(x)) * (b.Coord(y) - c.Coord(y));
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real ab = orient2d(a, b, point, x, y);
|
||||
const Standard_Real bc = orient2d(b, c, point, x, y);
|
||||
const Standard_Real ca = orient2d(c, a, point, x, y);
|
||||
|
||||
if ((ab >= 0) == (bc >= 0) && (ab >= 0) == (ca >= 0))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
Standard_Real linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y)
|
||||
{
|
||||
const Standard_Real aaS = orient2d(startA, endA, startB, x, y);
|
||||
const Standard_Real aaE = orient2d(startA, endA, endB, x, y);
|
||||
|
||||
if ((aaS >= 0) == (aaE >= 0))
|
||||
return false;
|
||||
|
||||
const Standard_Real bbS = orient2d(startB, endB, startA, x, y);
|
||||
const Standard_Real bbE = orient2d(startB, endB, endA, x, y);
|
||||
|
||||
if ((bbS >= 0) == (bbE >= 0))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
void getProjectionIndices(gp_Vec normal, PxU32& x, PxU32& y)
|
||||
{
|
||||
normal.SetCoord(std::abs(normal.X()), std::abs(normal.Y()), std::abs(normal.Z()));
|
||||
|
||||
if (normal.X() >= normal.Y() && normal.X() >= normal.Z())
|
||||
{
|
||||
//x is the dominant normal direction
|
||||
x = 1;
|
||||
y = 2;
|
||||
}
|
||||
else if (normal.Y() >= normal.X() && normal.Y() >= normal.Z())
|
||||
{
|
||||
//y is the dominant normal direction
|
||||
x = 2;
|
||||
y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
//z is the dominant normal direction
|
||||
x = 0;
|
||||
y = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2)
|
||||
{
|
||||
PxU32 x = 0;
|
||||
PxU32 y = 0;
|
||||
getProjectionIndices(p1_n, x, y);
|
||||
|
||||
const Standard_Real third = (1.0f / 3.0f);
|
||||
|
||||
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
|
||||
//difference is not very big and the coplanar case is not expected to be the most common case
|
||||
if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) ||
|
||||
linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) ||
|
||||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
|
||||
pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// Also with minor modification to return intersection points.
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar)
|
||||
{
|
||||
const Standard_Real tolerance = 1e-8f;
|
||||
|
||||
gp_Vec p1_n((b1 - a1).Crossed(c1 - a1).Normalized());
|
||||
double p1_d = -a1.Dot(p1_n);
|
||||
// const PxPlane p1(a1, b1, c1);
|
||||
const Standard_Real p1ToA = a2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToB = b2.Dot(p1_n) + p1_d;
|
||||
const Standard_Real p1ToC = c2.Dot(p1_n) + p1_d;
|
||||
|
||||
if(std::abs(p1ToA) < tolerance && std::abs(p1ToB) < tolerance &&std::abs(p1ToC) < tolerance)
|
||||
return ignoreCoplanar ? false : trianglesIntersectCoplanar(p1_n, a1, b1, c1, a2, b2, c2); //Coplanar triangles
|
||||
|
||||
if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0))
|
||||
return false; //All points of triangle 2 on same side of triangle 1 -> no intersection
|
||||
|
||||
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
|
||||
double p2_d = -a2.Dot(p2_n);
|
||||
// const PxPlane p2(a2, b2, c2);
|
||||
const Standard_Real p2ToA = a1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToB = b1.Dot(p2_n) + p2_d;
|
||||
const Standard_Real p2ToC = c1.Dot(p2_n) + p2_d;
|
||||
|
||||
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
|
||||
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
|
||||
|
||||
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
|
||||
const Standard_Real l2 = intersectionDirection.SquareMagnitude();
|
||||
intersectionDirection *= 1.0f / std::sqrt(l2);
|
||||
|
||||
const Interval i1 = computeInterval(p2ToA, p2ToB, p2ToC, a1, b1, c1, intersectionDirection);
|
||||
const Interval i2 = computeInterval(p1ToA, p1ToB, p1ToC, a2, b2, c2, intersectionDirection);
|
||||
|
||||
if (Interval::overlapOrTouch(i1, i2))
|
||||
{
|
||||
/*if (intersection)
|
||||
{
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
intersection->p0 = i.minPoint;
|
||||
intersection->p1 = i.maxPoint;
|
||||
}*/
|
||||
const Interval i = Interval::intersection(i1, i2);
|
||||
int1 = i.minPoint;
|
||||
int2 = i.maxPoint;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <gp_Vec.hxx>
|
||||
#include <array>
|
||||
|
||||
struct ray {
|
||||
float origin[3];
|
||||
float dir[3];
|
||||
float dir_inv[3];
|
||||
};
|
||||
|
||||
struct box {
|
||||
float corners[2][3];
|
||||
};
|
||||
|
||||
bool is_intersect_ray_box(const struct ray *ray, const struct box *box);
|
||||
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
Standard_Real& at, Standard_Real& au, Standard_Real& av,
|
||||
bool cull, float enlarge=0.0f);
|
||||
|
||||
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
|
||||
const gp_Vec& q, const gp_Vec& b); // seg 2 origin, vector
|
||||
|
||||
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q);
|
||||
|
||||
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar);
|
||||
@@ -109,7 +109,7 @@ endif
|
||||
mkdir -p dist/ifcopenshell
|
||||
cp -r ifcopenshell/* dist/ifcopenshell/
|
||||
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/
|
||||
ifeq ($(PLATFORM), win)
|
||||
|
||||
+37
-5
@@ -1,12 +1,44 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap");
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap');
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: normal;
|
||||
color: #7cbf33;
|
||||
background: -webkit-linear-gradient(0deg, #7cbf33, #049344);
|
||||
background-clip: border-box;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.toc-tree .reference:hover {
|
||||
color: #d98014;
|
||||
}
|
||||
.sidebar-brand-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.blockbutton {
|
||||
border: 1px solid var(--color-admonition-title--seealso);
|
||||
background-color: var(--color-admonition-title-background--seealso);
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.blockbutton a {
|
||||
display: block;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-radius: 5px;
|
||||
background: #40b74c;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.blockbutton a:hover {
|
||||
background: #70ba35;
|
||||
}
|
||||
.highlight .hll {
|
||||
background-color: #ffc2;
|
||||
}
|
||||
section img {
|
||||
display: block;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2020-2024 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# 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
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Lesser 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/>.
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
@@ -36,7 +36,7 @@ sys.path.insert(0, os.path.abspath('..'))
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "IfcOpenShell"
|
||||
copyright = "2011-2023, IfcOpenShell Contributors"
|
||||
copyright = "2011-2024, IfcOpenShell Contributors"
|
||||
author = "IfcOpenShell Contributors"
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
@@ -54,7 +54,7 @@ release = "0.7.0"
|
||||
# - No subnav making it really hard to navigate
|
||||
# - Kinda hacky setup https://stackoverflow.com/questions/2701998/sphinx-autodoc-is-not-automatic-enough
|
||||
# - I couldn't customise the template to show submodules above members which makes API discovery hard for users
|
||||
extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel"]
|
||||
extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel", "sphinx_copybutton"]
|
||||
|
||||
# Auto add document prefixes to help guarantee uniqueness of automatic section references.
|
||||
autosectionlabel_prefix_document = True
|
||||
@@ -67,6 +67,8 @@ autoapi_type = 'python'
|
||||
|
||||
# autoapi works by reading source code instead of importing modules
|
||||
autoapi_dirs = ['../ifcopenshell', '../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
|
||||
# autoapi_dirs = ['../../ifcdiff']
|
||||
# autoapi_dirs = ['../../ifcdiff', '../ifcopenshell/util']
|
||||
# autoapi_dirs = ['../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
|
||||
|
||||
# These are auto-generated based on the IFC schema, so exclude them
|
||||
@@ -113,3 +115,54 @@ html_css_files = ["custom.css"]
|
||||
# page even on light themes.
|
||||
pygments_style = "one-dark"
|
||||
pygments_dark_style = "one-dark"
|
||||
|
||||
html_logo = "https://ifcopenshell.org/assets/images/logo.png"
|
||||
html_theme_options = {
|
||||
"source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/",
|
||||
"source_branch": "v0.7.0",
|
||||
"source_directory": "src/ifcopenshell-python/docs/",
|
||||
"light_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#f7f7f6",
|
||||
"color-background-secondary": "#eeeeec",
|
||||
"color-background-border": "#cfd0cb",
|
||||
"color-foreground-primary": "#2e3436",
|
||||
"color-sidebar-item-background--hover": "#f7f7f6",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
"dark_css_variables": {
|
||||
"color-brand-primary": "#39b54a",
|
||||
"color-brand-content": "#39b54a",
|
||||
"color-brand-visited": "#d9e021",
|
||||
"color-background-primary": "#2e3436",
|
||||
"color-background-border": "#2e3436",
|
||||
"color-foreground-primary": "#eeeeec",
|
||||
"color-sidebar-item-background--hover": "#2e3436",
|
||||
"color-link--hover": "#d98014",
|
||||
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
|
||||
},
|
||||
|
||||
"footer_icons": [
|
||||
{
|
||||
"name": "IfcOpenShell",
|
||||
"url": "https://ifcopenshell.org",
|
||||
"html": """
|
||||
<img src="https://ifcopenshell.org/assets/images/logo.png" style="width: auto;" />
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcopenshell-python/docs",
|
||||
"html": """
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ Pre-built packages
|
||||
| build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ |
|
||||
+----------------+----------------+----------------+----------------+------------------+
|
||||
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-linux64.zip
|
||||
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win32.zip
|
||||
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win64.zip
|
||||
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macos64.zip
|
||||
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and run IfcConvert using the command line.
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
Geometry tree
|
||||
=============
|
||||
|
||||
IfcOpenShell includes a utility to build a unbalanced binary tree of geometry
|
||||
and their bounding boxes. After a tree is built, you can efficiently select
|
||||
geometry by specifying a point, radius, or bounding box.
|
||||
IfcOpenShell includes a utility to build trees of geometry and their bounding
|
||||
boxes. Geometry trees can be used to efficiently select geometry or collide
|
||||
geometry with one another.
|
||||
|
||||
.. image:: images/geometry-tree.png
|
||||
|
||||
The most efficient way to build tree is by using the iterator, as shown in the
|
||||
example below:
|
||||
The most efficient way to build a tree is by using the iterator. If the native
|
||||
OpenCASCADE shape is added to the tree, a **UB Tree** is built. Alternatively,
|
||||
if triangulation is added to the tree, a **BVH Tree** is built. The type of
|
||||
tree determines the type of operation you can perform.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -21,27 +23,208 @@ example below:
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
# Use triangulation to build a BVH tree
|
||||
tree.add_element(iterator.get())
|
||||
|
||||
# Alternatively, use this code to build an unbalanced binary tree
|
||||
# tree.add_element(iterator.get_native())
|
||||
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
Once built, there are three methods you can use to select elements in the tree:
|
||||
``select_box``, ``select``, and ``select_ray``.
|
||||
Clashing or selecting geometry from a geometry tree
|
||||
---------------------------------------------------
|
||||
|
||||
``select_box`` lets you query for elements that contain a point or another
|
||||
element. However, it only checks the bounding box of elements instead of their
|
||||
exact geometry. This is the fastest approach and is recommended if you don't
|
||||
need precise geometry selection.
|
||||
With a **BVH Tree**, you can efficiently clash sets of elements with other
|
||||
elements. You can find elements that intersect, collide, or are within a
|
||||
clearance distance threshold of one another. There are three methods you can
|
||||
use to clash elements in the tree. Each function collides one set of elements
|
||||
with another set of elements.
|
||||
|
||||
``select`` lets you query for elements that contain a point, a sphere, or
|
||||
another element. ``select`` is similar to select box, but additionally
|
||||
considers the actual geometry of the object. This is slower but more precise.
|
||||
- `Detecting intersection clashes between elements`_ detects when an element intersects with another
|
||||
element. This is the most common type of clash detection used when
|
||||
coordinating designs. For example, you might want to know if any pipes go
|
||||
through structural columns or beams.
|
||||
- `Detecting collision clashes between elements`_ detects when an element
|
||||
touches another element. It is the fastest type of clash detection but does
|
||||
not consider the distance that an element goes inside another element. This
|
||||
considers surfaces only so it works on non-manifold geometry but will not
|
||||
detect if an element is completely within another element.
|
||||
- `Detecting clearance clashes between elements`_ detects when an element comes
|
||||
near to another element within a clearance threshold. This is the slowest
|
||||
type of clash detection. It works on non-manifold geometry and does not
|
||||
consider inside vs outside. Elements like pipe and ducts with insulation,
|
||||
structural openings, and equipment will typically require clearance checks.
|
||||
|
||||
``select_ray`` lets you query for elements that intersect with a ray.
|
||||
With a **UB Tree**, you can efficiently select geometry by specifying a point,
|
||||
radius, or bounding box. There are three methods you can use to select elements
|
||||
in the tree.
|
||||
|
||||
- `Selecting elements using bounding boxes`_ lets you query for elements that
|
||||
contain a point or another element. However, it only checks the bounding box
|
||||
of elements instead of their exact geometry. This is the fastest approach and
|
||||
is recommended if you don't need precise geometry selection.
|
||||
- `Selecting elements using precise geometry`_ lets you query for elements that
|
||||
contain a point, a sphere, or another element. This is similar to selecting
|
||||
using bounding boxes, but additionally considers the actual geometry of the
|
||||
element. This is slower but more precise.
|
||||
- `Selecting elements using a ray`_ lets you query for elements that intersect
|
||||
with a ray.
|
||||
|
||||
Detecting intersection clashes between elements
|
||||
-----------------------------------------------
|
||||
|
||||
``clash_intersection_many`` detects when an element intersects with or is
|
||||
contained within another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_intersection_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
tolerance=0.002, # Any protrusions less than 2mm are ignored
|
||||
check_all=True, # Keep on checking all potential intersections to find a worst case protrusion distance
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# Potential clash types that can be detected are protrusions, pierces, and collisions
|
||||
clash_type = ["protrusion", "pierce", "collision", "clearance"][clash.clash_type],
|
||||
|
||||
# P1 and P2 represents two XYZ coordinates. The meaning of the coordinate depends on the clash type.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the protrusion or piercing distance in meters.
|
||||
# It is also the distance between P1 and P2.
|
||||
distance = clash.distance
|
||||
|
||||
If you specify a ``tolerance`` value, intersections with a protrusion distance
|
||||
smaller than this tolerance are excluded. It is recommended to specify a
|
||||
non-zero tolerance to distinguish between when elements merely touch (e.g. a
|
||||
GPO on a wall) versus if they are truly intersecting (e.g. a pipe going through
|
||||
a beam).
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as an
|
||||
intersection is found. This is faster but may not return the worst-case
|
||||
protrusion distance. If you are not interested in the protrusion distance, it
|
||||
is recommended to set this to ``False``. If you want the protrusion distance,
|
||||
such as to prioritise which clashes are more severe, set this to ``True``.
|
||||
|
||||
This includes:
|
||||
|
||||
1. When an element X protrudes inside element Y, where element Y is manifold.
|
||||
In this case, a protrusion distance is calculated as the deepest point of
|
||||
element X to the closest surface of element Y. ``P1`` is defined as the XYZ
|
||||
coordinate on element X, and ``P2`` is defined as the nearest point on the
|
||||
surface of element Y.
|
||||
2. When an element X pierces element Y, such that an edge of element X enters
|
||||
element Y and leaves through another face. In this case, a piercing distance
|
||||
is calculated as the distance where that edge is inside element Y. ``P1`` is
|
||||
defined as the point on an edge of element X which enters element Y, and
|
||||
``P2`` is the point where that edge leaves element Y.
|
||||
3. When neither X or Y is manifold, we cannot detect protrusion or piercing, so
|
||||
instead when X and Y have any touching face. This is the same as the
|
||||
``clash_collision_many`` check below. The distance is considered to be zero
|
||||
and ignores your specified tolerance. ``P1`` and ``P2`` are equal and
|
||||
represent an arbitrary XYZ point where the two elements touch.
|
||||
|
||||
Detecting collision clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_collision_many`` detects when the surface of an element collides with
|
||||
another element. The surfaces may either merely touch (e.g. are coplanar) or
|
||||
intersect.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_collision_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
allow_touching=True, # Include results where faces merely touch but do not intersect
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents two possible arbitrary points where a collision is found.
|
||||
# P1 may or may not be equal to P2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
A collision between two surface triangles may be "touching" or "intersecting".
|
||||
Two touching triangles may be coplanar or merely have a single edge or vertex
|
||||
touching the other triangle. An intersecting triangle will have at least one
|
||||
edge that goes through the other triangle.
|
||||
|
||||
Detecting clearance clashes between elements
|
||||
--------------------------------------------
|
||||
|
||||
``clash_clearance_many`` detects with the surface of an element comes within a
|
||||
clearance distance threshold of another element.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
clashes = tree.clash_clearance_many(
|
||||
group_a_elements, # e.g. from model.by_type("IfcWall")
|
||||
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
|
||||
clearance=0.1, # Any surface closer than than 100mm is a clash
|
||||
check_all=False, # Stop measuring distances once the first clearance violation is found per element.
|
||||
)
|
||||
|
||||
for clash in clashes:
|
||||
# Get the two elements that clash and their metadata
|
||||
element1 = clash.a
|
||||
element2 = clash.b
|
||||
a_global_id = element1.get_argument(0)
|
||||
b_global_id = element2.get_argument(0)
|
||||
a_ifc_class = element1.is_a()
|
||||
b_ifc_class = element2.is_a()
|
||||
a_name = element1.get_argument(2)
|
||||
b_name = element2.get_argument(2)
|
||||
|
||||
# P1 and P2 represents the two XYZ coordinates between element1 and element2.
|
||||
p1 = list(clash.p1)
|
||||
p2 = list(clash.p2)
|
||||
|
||||
# This represents the distance between element1 and element2 that is less than the clearance.
|
||||
# It is the distance between P1 and P2. It cannot be less than 0.
|
||||
distance = clash.distance
|
||||
|
||||
You cannot specify a ``clearance`` less than 0.
|
||||
|
||||
If ``check_all`` is ``False``, the clash check will return as soon as a
|
||||
clearance violation is found. This is faster but may not return the worst-case
|
||||
distance. If you only interested whether there is a clearance issue, it is
|
||||
recommended to set this to ``False``. If you want the exact worst case
|
||||
clearance distance, such as to prioritise which clashes are more severe, set
|
||||
this to ``True``.
|
||||
|
||||
Selecting elements using bounding boxes
|
||||
---------------------------------------
|
||||
|
||||
Elements may be queried using an axis aligned bounding box. An axis aligned
|
||||
bounding box is the bounding box using global XYZ axes, not the element's local
|
||||
XYZ axes. If you have a vertical construction project, this means that your
|
||||
model should be oriented to project north to get the best results.
|
||||
|
||||
You may select all elements that have a bounding box containing the point with
|
||||
XYZ coordinates of ``(0., 0., 0.)``.
|
||||
|
||||
@@ -128,11 +311,11 @@ geometry. It will return:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
elements = tree.select_box(wall, completely_within=True)
|
||||
elements = tree.select(wall, completely_within=True)
|
||||
|
||||
# Alternatively, you may also specify an extension to dilate the geometry
|
||||
# of the wall.
|
||||
elements = tree.select_box(wall, completely_within=True, extend=5.)
|
||||
elements = tree.select(wall, completely_within=True, extend=5.)
|
||||
|
||||
Selecting elements using a ray
|
||||
------------------------------
|
||||
|
||||
@@ -30,10 +30,6 @@ the API.
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| | Linux 64bit | Windows 32bit | Windows 64bit | MacOS Intel 64bit | MacOS Silicon 64bit |
|
||||
+=============+================+================+================+===================+=====================+
|
||||
| Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
| Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ |
|
||||
@@ -43,36 +39,26 @@ the API.
|
||||
| Python 3.12 | py312-linux64_ | py312-win32_ | py312-win64_ | py312-macos64_ | py312-macosm164_ |
|
||||
+-------------+----------------+----------------+----------------+-------------------+---------------------+
|
||||
|
||||
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-linux64.zip
|
||||
.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win32.zip
|
||||
.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win32.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win32.zip
|
||||
.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win64.zip
|
||||
.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win64.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win64.zip
|
||||
.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macos64.zip
|
||||
.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macosm164.zip
|
||||
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-linux64.zip
|
||||
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win32.zip
|
||||
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win32.zip
|
||||
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win32.zip
|
||||
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win32.zip
|
||||
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win64.zip
|
||||
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win64.zip
|
||||
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win64.zip
|
||||
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win64.zip
|
||||
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macos64.zip
|
||||
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macosm164.zip
|
||||
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macosm164.zip
|
||||
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macosm164.zip
|
||||
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macosm164.zip
|
||||
|
||||
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
|
||||
Python path. If you're not sure where your Python path is, run the following
|
||||
|
||||
@@ -233,4 +233,4 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
|
||||
"``concat({{value}}[, {{value2}}]*)``", "``concat(""foo"", ""bar"")``", "``foobar``", "Concatenates two or more strings."
|
||||
"``round({{value}}, {{precision}})``", "``round(3.123, 0.1)``", "``3.1``", "Rounds ``{{value}}`` to the nearest ``{{precision}}``."
|
||||
"``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places."
|
||||
"``imperial_length({{value}}, {{precision}}, {{unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "Rounds ``{{value}}`` to the nearest ``1/{{precision}}`` inch then displays using fractional feet and inches. The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{unit}}``."
|
||||
"``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "``The {{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot`` or just inches if ``{{output_unit}}`` is set to ``inch``."
|
||||
|
||||
@@ -61,7 +61,8 @@ operating systems. GCC (4.7 or newer) or Clang (any version) is required.
|
||||
|
||||
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
|
||||
|
||||
3. Install OpenCascade Technology (OCCT).
|
||||
3. Install OpenCascade Technology (OCCT). Officially v7.5.0 is supported. Other
|
||||
versions may have unexpected behaviour.
|
||||
|
||||
::
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user