mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 22:33:33 +00:00
Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into update_type_manager
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
name: Deploy Pyodide Demo App to GitHub Pages
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
pages: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/pyodide/**'
|
||||||
|
- '.github/workflows/publish-pyodide-demo-app.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
activate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||||
|
steps:
|
||||||
|
- name: Set env
|
||||||
|
run: echo ok go
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: activate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout (recursive)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
- name: Upload static files as artifact
|
||||||
|
id: deployment
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: src/pyodide/demo-app/
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -98,3 +98,5 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
|
|||||||
|
|
||||||
# Brickschema
|
# Brickschema
|
||||||
src/bonsai/bonsai/bim/schema/Brick.ttl
|
src/bonsai/bonsai/bim/schema/Brick.ttl
|
||||||
|
bonsaiDecoratorForLoads.code-workspace
|
||||||
|
dev_environment.bat
|
||||||
|
|||||||
@@ -20,3 +20,6 @@
|
|||||||
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
|
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
|
||||||
path = src/ifcopenshell-python/ifcopenshell/simple_spf
|
path = src/ifcopenshell-python/ifcopenshell/simple_spf
|
||||||
url = https://github.com/IfcOpenShell/step-file-parser
|
url = https://github.com/IfcOpenShell/step-file-parser
|
||||||
|
[submodule "src/pyodide/demo-app/wheels"]
|
||||||
|
path = src/pyodide/demo-app/wheels
|
||||||
|
url = https://github.com/IfcOpenShell/wasm-wheels
|
||||||
|
|||||||
Binary file not shown.
@@ -134,7 +134,7 @@ def update_bim_tool_props():
|
|||||||
if AuthoringData.data["active_material_usage"] == "LAYER2":
|
if AuthoringData.data["active_material_usage"] == "LAYER2":
|
||||||
x_angle = get_x_angle(extrusion)
|
x_angle = get_x_angle(extrusion)
|
||||||
axis = tool.Model.get_wall_axis(obj)["reference"]
|
axis = tool.Model.get_wall_axis(obj)["reference"]
|
||||||
props.extrusion_depth = extrusion.Depth * si_conversion * cos(x_angle)
|
props.extrusion_depth = abs(extrusion.Depth * si_conversion * cos(x_angle))
|
||||||
props.length = (axis[1] - axis[0]).length
|
props.length = (axis[1] - axis[0]).length
|
||||||
props.x_angle = x_angle
|
props.x_angle = x_angle
|
||||||
|
|
||||||
|
|||||||
@@ -261,8 +261,6 @@ class IfcImporter:
|
|||||||
self.profile_code("Setup arrays")
|
self.profile_code("Setup arrays")
|
||||||
tool.Project.load_linked_models_from_ifc()
|
tool.Project.load_linked_models_from_ifc()
|
||||||
self.profile_code("Load linked models")
|
self.profile_code("Load linked models")
|
||||||
self.lock_scales()
|
|
||||||
self.profile_code("Lock objects scales")
|
|
||||||
self.add_project_to_scene()
|
self.add_project_to_scene()
|
||||||
self.profile_code("Add project to scene")
|
self.profile_code("Add project to scene")
|
||||||
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
||||||
@@ -1130,21 +1128,6 @@ class IfcImporter:
|
|||||||
bpy.context.scene.BIMAggregateProperties.aggregate_decorator = True
|
bpy.context.scene.BIMAggregateProperties.aggregate_decorator = True
|
||||||
|
|
||||||
|
|
||||||
def lock_scales(self) -> None:
|
|
||||||
elements = set(self.file.by_type("IfcProduct"))
|
|
||||||
while elements:
|
|
||||||
element = elements.pop()
|
|
||||||
if not getattr(element, "HasOpenings", False):
|
|
||||||
continue
|
|
||||||
voided_elements = tool.Aggregate.get_parts_recursively(element)
|
|
||||||
voided_elements.add(element)
|
|
||||||
elements.difference_update(voided_elements)
|
|
||||||
for element in voided_elements:
|
|
||||||
if not (obj := tool.Ifc.get_object(element)):
|
|
||||||
continue
|
|
||||||
tool.Geometry.lock_scale(obj)
|
|
||||||
|
|
||||||
|
|
||||||
class IfcImportSettings:
|
class IfcImportSettings:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logger: logging.Logger = None
|
self.logger: logging.Logger = None
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ class AggregateDecorator:
|
|||||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
|
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
|
||||||
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
||||||
|
|
||||||
|
|
||||||
indices, edges = create_bounding_box(parts_objs)
|
indices, edges = create_bounding_box(parts_objs)
|
||||||
self.line_shader.uniform_float("lineWidth", 0.5)
|
self.line_shader.uniform_float("lineWidth", 0.5)
|
||||||
self.draw_batch("LINES", indices, color, edges)
|
self.draw_batch("LINES", indices, color, edges)
|
||||||
@@ -230,9 +229,7 @@ class AggregateModeDecorator:
|
|||||||
cls.handlers.append(
|
cls.handlers.append(
|
||||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
|
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
|
||||||
)
|
)
|
||||||
cls.handlers.append(
|
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_new_objects, (context,), "WINDOW", "POST_VIEW"))
|
||||||
SpaceView3D.draw_handler_add(handler.draw_new_objects, (context,), "WINDOW", "POST_VIEW")
|
|
||||||
)
|
|
||||||
cls.is_installed = True
|
cls.is_installed = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -352,12 +349,11 @@ class AggregateModeDecorator:
|
|||||||
new_objs = [o for o in new_objs if o.data]
|
new_objs = [o for o in new_objs if o.data]
|
||||||
for obj in new_objs:
|
for obj in new_objs:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(props.editing_aggregate):
|
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(
|
||||||
|
props.editing_aggregate
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
if element and element.is_a("IfcElement"):
|
if element and element.is_a("IfcElement"):
|
||||||
data = ItemDecorator.get_obj_data(obj)
|
data = ItemDecorator.get_obj_data(obj)
|
||||||
if data:
|
if data:
|
||||||
self.draw_batch(
|
self.draw_batch("TRIS", data["verts"], transparent_color((1, 0, 0, 1)), data["tris"])
|
||||||
"TRIS", data["verts"], transparent_color((1, 0, 0, 1)), data["tris"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
|
|||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
||||||
bl_idname = "bim.disable_aggregate_mode"
|
bl_idname = "bim.disable_aggregate_mode"
|
||||||
bl_label = "Disable Aggregate Mode"
|
bl_label = "Disable Aggregate Mode"
|
||||||
@@ -385,6 +386,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
|||||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
||||||
bl_idname = "bim.toggle_aggregate_mode_local_view"
|
bl_idname = "bim.toggle_aggregate_mode_local_view"
|
||||||
bl_label = "Toggle Aggregate Mode Local View"
|
bl_label = "Toggle Aggregate Mode Local View"
|
||||||
@@ -403,6 +405,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
|||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
|
class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
|
||||||
bl_idname = "bim.aggregate_assign_new_objects_in_aggregate_mode"
|
bl_idname = "bim.aggregate_assign_new_objects_in_aggregate_mode"
|
||||||
bl_label = "Aggregate Assign New Objects In Aggregate Mode"
|
bl_label = "Aggregate Assign New Objects In Aggregate Mode"
|
||||||
@@ -424,7 +427,9 @@ class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
|
|||||||
new_objs = [o for o in new_objs if o.data]
|
new_objs = [o for o in new_objs if o.data]
|
||||||
for obj in new_objs:
|
for obj in new_objs:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(props.editing_aggregate):
|
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(
|
||||||
|
props.editing_aggregate
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
if element and element.is_a("IfcElement"):
|
if element and element.is_a("IfcElement"):
|
||||||
obj.select_set(True)
|
obj.select_set(True)
|
||||||
@@ -439,4 +444,3 @@ class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
|
|||||||
BIM_OT_aggregate_assign_object._execute(self, context)
|
BIM_OT_aggregate_assign_object._execute(self, context)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -313,10 +313,8 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
|
|||||||
if not tool.Ifc.get():
|
if not tool.Ifc.get():
|
||||||
return False
|
return False
|
||||||
props = context.scene.BIMMaterialProperties
|
props = context.scene.BIMMaterialProperties
|
||||||
if props.materials and props.active_material_index < len(props.materials):
|
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
|
||||||
material = props.materials[props.active_material_index]
|
return True
|
||||||
if material.ifc_definition_id:
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from . import ui, prop, operator
|
from . import ui, prop, operator
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
operator.AddCurvelikeItem,
|
operator.AddCurvelikeItem,
|
||||||
@@ -89,7 +90,20 @@ classes = (
|
|||||||
addon_keymaps = []
|
addon_keymaps = []
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def block_scale(scene):
|
||||||
|
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
|
||||||
|
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
|
||||||
|
if obj.scale != (1, 1, 1):
|
||||||
|
obj.scale = (1, 1, 1)
|
||||||
|
elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
|
||||||
|
if obj.scale != (1, 1, 1):
|
||||||
|
obj.scale = (1, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
bpy.app.handlers.depsgraph_update_pre.append(block_scale)
|
||||||
|
|
||||||
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
|
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
|
||||||
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
|
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
|
||||||
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
||||||
@@ -158,6 +172,8 @@ def register():
|
|||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
bpy.app.handlers.depsgraph_update_pre.remove(block_scale)
|
||||||
|
|
||||||
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
|
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
|
||||||
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
|
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
|
||||||
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
|
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ class Helper:
|
|||||||
x_axis = (mesh.vertices[loop[0]].co - center).normalized()
|
x_axis = (mesh.vertices[loop[0]].co - center).normalized()
|
||||||
else:
|
else:
|
||||||
x_axis = (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).normalized()
|
x_axis = (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).normalized()
|
||||||
z_axis = profile_face.polygons[0].normal.normalized() * (-1)
|
z_axis = profile_face.polygons[0].normal.normalized() * -1
|
||||||
y_axis = z_axis.cross(x_axis).normalized()
|
y_axis = z_axis.cross(x_axis).normalized()
|
||||||
matrix = Matrix((x_axis, y_axis, z_axis))
|
matrix = Matrix((x_axis, y_axis, z_axis))
|
||||||
matrix.normalize()
|
matrix.normalize()
|
||||||
|
|||||||
@@ -1858,7 +1858,7 @@ class OverrideEscape(bpy.types.Operator):
|
|||||||
bpy.ops.bim.hide_all_openings()
|
bpy.ops.bim.hide_all_openings()
|
||||||
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
|
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||||
bpy.ops.bim.disable_aggregate_mode()
|
bpy.ops.bim.disable_aggregate_mode()
|
||||||
elif active_object:=context.active_object:
|
elif active_object := context.active_object:
|
||||||
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
|
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
|
||||||
pass
|
pass
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
@@ -1902,7 +1902,9 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
|
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
|
||||||
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
|
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
|
||||||
obj.select_set(False)
|
obj.select_set(False)
|
||||||
self.report({"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly")
|
self.report(
|
||||||
|
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
|
||||||
|
)
|
||||||
elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
|
elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
|
||||||
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
||||||
elif element:
|
elif element:
|
||||||
@@ -1971,6 +1973,13 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ProfileDecorator.install(context)
|
ProfileDecorator.install(context)
|
||||||
if not bpy.app.background:
|
if not bpy.app.background:
|
||||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||||
|
elif item.is_a("IfcAnnotationFillArea"):
|
||||||
|
tool.Model.import_annotation_fill_area(item, obj=obj)
|
||||||
|
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||||
|
self.enable_edit_mode(context)
|
||||||
|
ProfileDecorator.install(context)
|
||||||
|
if not bpy.app.background:
|
||||||
|
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||||
elif tool.Geometry.is_curvelike_item(item):
|
elif tool.Geometry.is_curvelike_item(item):
|
||||||
tool.Model.import_curve(item, obj=obj)
|
tool.Model.import_curve(item, obj=obj)
|
||||||
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||||
@@ -2124,10 +2133,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
tool.Geometry.import_item(obj)
|
tool.Geometry.import_item(obj)
|
||||||
elif item.is_a("IfcSweptAreaSolid"):
|
elif item.is_a("IfcSweptAreaSolid"):
|
||||||
ProfileDecorator.uninstall()
|
ProfileDecorator.uninstall()
|
||||||
|
if not (profile := tool.Model.export_profile(obj)):
|
||||||
profile = tool.Model.export_profile(obj)
|
|
||||||
|
|
||||||
if not profile:
|
|
||||||
|
|
||||||
def msg(self, context):
|
def msg(self, context):
|
||||||
self.layout.label(text="INVALID PROFILE")
|
self.layout.label(text="INVALID PROFILE")
|
||||||
@@ -2184,6 +2190,26 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
product=element,
|
product=element,
|
||||||
representation=new_footprint,
|
representation=new_footprint,
|
||||||
)
|
)
|
||||||
|
elif item.is_a("IfcAnnotationFillArea"):
|
||||||
|
ProfileDecorator.uninstall()
|
||||||
|
if not (profile := tool.Model.export_annotation_fill_area(obj)):
|
||||||
|
|
||||||
|
def msg(self, context):
|
||||||
|
self.layout.label(text="INVALID PROFILE")
|
||||||
|
|
||||||
|
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||||
|
ProfileDecorator.install(bpy.context)
|
||||||
|
self.enable_edit_mode(bpy.context)
|
||||||
|
return
|
||||||
|
|
||||||
|
for inverse in tool.Ifc.get().get_inverse(item):
|
||||||
|
ifcopenshell.util.element.replace_attribute(inverse, item, profile)
|
||||||
|
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
|
||||||
|
obj.data.BIMMeshProperties.ifc_definition_id = profile.id()
|
||||||
|
|
||||||
|
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
|
||||||
|
tool.Geometry.import_item(obj)
|
||||||
|
tool.Geometry.import_item_attributes(obj)
|
||||||
elif tool.Geometry.is_curvelike_item(item):
|
elif tool.Geometry.is_curvelike_item(item):
|
||||||
ProfileDecorator.uninstall()
|
ProfileDecorator.uninstall()
|
||||||
new = tool.Model.export_curves(obj)
|
new = tool.Model.export_curves(obj)
|
||||||
@@ -2924,7 +2950,7 @@ class OverrideMoveAggregate(bpy.types.Operator):
|
|||||||
obj.select_set(False)
|
obj.select_set(False)
|
||||||
continue
|
continue
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if not element or props.in_aggregate_mode:
|
if not element or not element.is_a("IfcElement") or props.in_aggregate_mode:
|
||||||
continue
|
continue
|
||||||
parts = ifcopenshell.util.element.get_parts(element)
|
parts = ifcopenshell.util.element.get_parts(element)
|
||||||
if parts:
|
if parts:
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ class BIMMaterialProperties(PropertyGroup):
|
|||||||
styles: EnumProperty(items=get_styles, name="Styles")
|
styles: EnumProperty(items=get_styles, name="Styles")
|
||||||
contexts: EnumProperty(items=get_contexts, name="Contexts")
|
contexts: EnumProperty(items=get_contexts, name="Contexts")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_material(self):
|
||||||
|
if self.active_material_index < len(self.materials):
|
||||||
|
return self.materials[self.active_material_index]
|
||||||
|
|
||||||
|
|
||||||
class BIMObjectMaterialProperties(PropertyGroup):
|
class BIMObjectMaterialProperties(PropertyGroup):
|
||||||
material_type: EnumProperty(items=get_object_material_type, name="Material Type")
|
material_type: EnumProperty(items=get_object_material_type, name="Material Type")
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ classes = (
|
|||||||
profile.ChangeCardinalPoint,
|
profile.ChangeCardinalPoint,
|
||||||
profile.ChangeProfileDepth,
|
profile.ChangeProfileDepth,
|
||||||
profile.DisableEditingExtrusionAxis,
|
profile.DisableEditingExtrusionAxis,
|
||||||
|
profile.DrawPolylineProfile,
|
||||||
profile.EditExtrusionAxis,
|
profile.EditExtrusionAxis,
|
||||||
profile.EnableEditingExtrusionAxis,
|
profile.EnableEditingExtrusionAxis,
|
||||||
profile.ExtendProfile,
|
profile.ExtendProfile,
|
||||||
@@ -127,6 +128,7 @@ classes = (
|
|||||||
prop.SnapMousePoint,
|
prop.SnapMousePoint,
|
||||||
prop.PolylinePoint,
|
prop.PolylinePoint,
|
||||||
prop.Polyline,
|
prop.Polyline,
|
||||||
|
prop.ProductPreviewItem,
|
||||||
prop.BIMModelProperties,
|
prop.BIMModelProperties,
|
||||||
prop.BIMArrayProperties,
|
prop.BIMArrayProperties,
|
||||||
prop.BIMStairProperties,
|
prop.BIMStairProperties,
|
||||||
@@ -136,6 +138,7 @@ classes = (
|
|||||||
prop.BIMRailingProperties,
|
prop.BIMRailingProperties,
|
||||||
prop.BIMRoofProperties,
|
prop.BIMRoofProperties,
|
||||||
prop.BIMPolylineProperties,
|
prop.BIMPolylineProperties,
|
||||||
|
prop.BIMProductPreviewProperties,
|
||||||
ui.BIM_PT_array,
|
ui.BIM_PT_array,
|
||||||
ui.BIM_PT_stair,
|
ui.BIM_PT_stair,
|
||||||
ui.BIM_PT_sverchok,
|
ui.BIM_PT_sverchok,
|
||||||
@@ -218,6 +221,7 @@ def register():
|
|||||||
|
|
||||||
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
||||||
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
|
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
|
||||||
|
bpy.types.Scene.BIMProductPreviewProperties = bpy.props.PointerProperty(type=prop.BIMProductPreviewProperties)
|
||||||
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
|
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
|
||||||
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
|
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
|
||||||
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
|
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
|
||||||
@@ -248,6 +252,7 @@ def unregister():
|
|||||||
|
|
||||||
del bpy.types.Scene.BIMModelProperties
|
del bpy.types.Scene.BIMModelProperties
|
||||||
del bpy.types.Scene.BIMPolylineProperties
|
del bpy.types.Scene.BIMPolylineProperties
|
||||||
|
del bpy.types.Scene.BIMProductPreviewProperties
|
||||||
del bpy.types.Object.BIMArrayProperties
|
del bpy.types.Object.BIMArrayProperties
|
||||||
del bpy.types.Object.BIMStairProperties
|
del bpy.types.Object.BIMStairProperties
|
||||||
del bpy.types.Object.BIMSverchokProperties
|
del bpy.types.Object.BIMSverchokProperties
|
||||||
|
|||||||
@@ -25,15 +25,14 @@ import bmesh
|
|||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
import math
|
import math
|
||||||
from math import sin, cos, tan, radians
|
from math import sin, cos, radians
|
||||||
from bpy.types import SpaceView3D
|
from bpy.types import SpaceView3D
|
||||||
from bpy_extras import view3d_utils
|
from bpy_extras import view3d_utils
|
||||||
from mathutils import Vector, Matrix, Quaternion
|
from mathutils import Vector, Matrix
|
||||||
from gpu_extras.batch import batch_for_shader
|
from gpu_extras.batch import batch_for_shader
|
||||||
from gpu_extras.presets import draw_circle_2d
|
from gpu_extras.presets import draw_circle_2d
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from bonsai.bim.module.drawing.helper import format_distance
|
from bonsai.bim.module.drawing.helper import format_distance
|
||||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
|
||||||
|
|
||||||
|
|
||||||
def transparent_color(color, alpha=0.1):
|
def transparent_color(color, alpha=0.1):
|
||||||
@@ -786,6 +785,10 @@ class ProductDecorator:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def uninstall(cls):
|
def uninstall(cls):
|
||||||
|
props = bpy.context.scene.BIMProductPreviewProperties # updated by model/polyline.py
|
||||||
|
props.verts.clear()
|
||||||
|
props.edges.clear()
|
||||||
|
props.tris.clear()
|
||||||
for handler in cls.handlers:
|
for handler in cls.handlers:
|
||||||
try:
|
try:
|
||||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||||
@@ -799,315 +802,12 @@ class ProductDecorator:
|
|||||||
shader.uniform_float("color", color)
|
shader.uniform_float("color", color)
|
||||||
batch.draw(shader)
|
batch.draw(shader)
|
||||||
|
|
||||||
def get_wall_preview_data(cls, context, relating_type):
|
def get_product_preview_data(self, context):
|
||||||
def create_bmesh_from_vertices(vertices):
|
props = context.scene.BIMProductPreviewProperties
|
||||||
bm = bmesh.new()
|
|
||||||
|
|
||||||
new_verts = [bm.verts.new(v) for v in polyline_vertices]
|
|
||||||
if is_closed:
|
|
||||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
|
||||||
new_edges.append(
|
|
||||||
bm.edges.new((new_verts[-1], new_verts[0]))
|
|
||||||
) # Add an edge between the last an first point to make it closed.
|
|
||||||
else:
|
|
||||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
|
||||||
|
|
||||||
bm.verts.index_update()
|
|
||||||
bm.edges.index_update()
|
|
||||||
return bm
|
|
||||||
|
|
||||||
# Get properties from object type
|
|
||||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
|
||||||
if not layers["thickness"]:
|
|
||||||
return
|
|
||||||
thickness = layers["thickness"]
|
|
||||||
model_props = context.scene.BIMModelProperties
|
|
||||||
direction_sense = model_props.direction_sense
|
|
||||||
direction = 1
|
|
||||||
if direction_sense == "NEGATIVE":
|
|
||||||
direction = -1
|
|
||||||
offset_type = model_props.offset_type
|
|
||||||
offset = 0
|
|
||||||
if offset_type == "CENTER":
|
|
||||||
offset = -thickness / 2
|
|
||||||
elif offset_type == "INTERIOR":
|
|
||||||
offset = -thickness
|
|
||||||
|
|
||||||
unit_system = tool.Drawing.get_unit_system()
|
|
||||||
factor = 1
|
|
||||||
if unit_system == "IMPERIAL":
|
|
||||||
factor = 3.048
|
|
||||||
if unit_system == "METRIC":
|
|
||||||
unit_length = context.scene.unit_settings.length_unit
|
|
||||||
if unit_length == "MILLIMETERS":
|
|
||||||
factor = 1000
|
|
||||||
|
|
||||||
# For the model properties, the offset value should just be converted
|
|
||||||
# However, for the wall preview logic that follows, offset and thickness must change direction
|
|
||||||
model_props.offset = offset * factor
|
|
||||||
thickness *= direction
|
|
||||||
offset *= direction
|
|
||||||
|
|
||||||
height = float(model_props.extrusion_depth)
|
|
||||||
rl = float(model_props.rl1)
|
|
||||||
x_angle = float(model_props.x_angle)
|
|
||||||
angle_distortion = height * tan(x_angle)
|
|
||||||
|
|
||||||
wall_preview_data = {}
|
|
||||||
wall_preview_data["verts"] = []
|
|
||||||
|
|
||||||
# Verts
|
|
||||||
polyline_vertices = []
|
|
||||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
|
||||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
|
||||||
if len(polyline_points) < 2:
|
|
||||||
wall_preview_data = []
|
|
||||||
return
|
|
||||||
for point in polyline_points:
|
|
||||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
|
||||||
|
|
||||||
is_closed = False
|
|
||||||
if (
|
|
||||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
|
||||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
|
||||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
|
||||||
):
|
|
||||||
is_closed = True
|
|
||||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
|
||||||
|
|
||||||
bm_base = create_bmesh_from_vertices(polyline_vertices)
|
|
||||||
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
|
||||||
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
|
||||||
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
|
|
||||||
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
|
|
||||||
if is_closed:
|
|
||||||
base_vertices.append(base_vertices[0])
|
|
||||||
offset_base_verts.append(offset_base_verts[0])
|
|
||||||
top_vertices.append(top_vertices[0])
|
|
||||||
offset_top_verts.append(offset_top_verts[0])
|
|
||||||
|
|
||||||
if offset_base_verts is not None:
|
|
||||||
for v in base_vertices:
|
|
||||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
|
||||||
|
|
||||||
for v in offset_base_verts[::-1]:
|
|
||||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
|
||||||
|
|
||||||
for v in top_vertices:
|
|
||||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
|
||||||
|
|
||||||
for v in offset_top_verts[::-1]:
|
|
||||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
|
||||||
|
|
||||||
bm_base.free()
|
|
||||||
|
|
||||||
# Edges and Tris
|
|
||||||
points = []
|
|
||||||
side_edges_1 = []
|
|
||||||
side_edges_2 = []
|
|
||||||
base_edges = []
|
|
||||||
|
|
||||||
for i in range(len(wall_preview_data["verts"])):
|
|
||||||
points.append(Vector(wall_preview_data["verts"][i]))
|
|
||||||
|
|
||||||
n = len(points) // 2
|
|
||||||
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
|
||||||
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
|
||||||
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
|
||||||
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
|
||||||
side_edges_1.extend(bottom_side_1)
|
|
||||||
side_edges_2.extend(bottom_side_2)
|
|
||||||
base_edges.extend(bottom_loop)
|
|
||||||
|
|
||||||
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
|
||||||
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
|
||||||
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
|
||||||
side_edges_1.extend(upper_side_1)
|
|
||||||
side_edges_2.extend(upper_side_2)
|
|
||||||
base_edges.extend(upper_loop)
|
|
||||||
|
|
||||||
loops = [side_edges_1, side_edges_2, base_edges]
|
|
||||||
|
|
||||||
wall_preview_data["edges"] = []
|
|
||||||
wall_preview_data["tris"] = []
|
|
||||||
for i, group in enumerate(loops):
|
|
||||||
bm = bmesh.new()
|
|
||||||
|
|
||||||
new_verts = [bm.verts.new(v) for v in points]
|
|
||||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
|
||||||
|
|
||||||
bm.verts.index_update()
|
|
||||||
bm.edges.index_update()
|
|
||||||
|
|
||||||
if i == 2:
|
|
||||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
|
||||||
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
|
||||||
|
|
||||||
bm.verts.index_update()
|
|
||||||
bm.edges.index_update()
|
|
||||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
|
||||||
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
|
||||||
wall_preview_data["edges"].extend(edges)
|
|
||||||
wall_preview_data["tris"].extend(tris)
|
|
||||||
|
|
||||||
wall_preview_data["edges"] = list(set(tuple(e) for e in wall_preview_data["edges"]))
|
|
||||||
wall_preview_data["tris"] = list(set(tuple(t) for t in wall_preview_data["tris"]))
|
|
||||||
|
|
||||||
return wall_preview_data
|
|
||||||
|
|
||||||
def get_product_preview_data(cls, context, relating_type):
|
|
||||||
model_props = context.scene.BIMModelProperties
|
|
||||||
if relating_type.is_a("IfcDoorType"):
|
|
||||||
rl = float(model_props.rl1)
|
|
||||||
elif relating_type.is_a("IfcWindowType"):
|
|
||||||
rl = float(model_props.rl2)
|
|
||||||
else:
|
|
||||||
rl = 0
|
|
||||||
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
|
||||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
|
||||||
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
|
||||||
snap_element = tool.Ifc.get_entity(snap_obj)
|
|
||||||
rot_mat = Quaternion()
|
|
||||||
if snap_element and snap_element.is_a("IfcWall"):
|
|
||||||
rot_mat = snap_obj.matrix_world.to_quaternion()
|
|
||||||
|
|
||||||
obj_type = tool.Ifc.get_object(relating_type)
|
|
||||||
if obj_type.data:
|
|
||||||
data = ItemDecorator.get_obj_data(obj_type)
|
|
||||||
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
|
|
||||||
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_profile_preview_data(self, context, relating_type):
|
|
||||||
material = ifcopenshell.util.element.get_material(relating_type)
|
|
||||||
try:
|
|
||||||
profile = material.MaterialProfiles[0].Profile
|
|
||||||
except:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
model_props = context.scene.BIMModelProperties
|
|
||||||
extrusion_depth = model_props.extrusion_depth
|
|
||||||
cardinal_point = model_props.cardinal_point
|
|
||||||
rot_mat = Quaternion()
|
|
||||||
if relating_type.is_a("IfcBeamType"):
|
|
||||||
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
|
|
||||||
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
|
|
||||||
rot_mat = y_rot @ z_rot
|
|
||||||
# Get profile data
|
|
||||||
settings = ifcopenshell.geom.settings()
|
|
||||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
|
||||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
|
||||||
|
|
||||||
verts = shape.verts
|
|
||||||
if not verts:
|
|
||||||
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
|
|
||||||
|
|
||||||
edges = shape.edges
|
|
||||||
|
|
||||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
|
||||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
|
||||||
|
|
||||||
# Create offsets based on cardinal point
|
|
||||||
min_x = min(v[0] for v in grouped_verts)
|
|
||||||
max_x = max(v[0] for v in grouped_verts)
|
|
||||||
min_y = min(v[1] for v in grouped_verts)
|
|
||||||
max_y = max(v[1] for v in grouped_verts)
|
|
||||||
|
|
||||||
x_offset = (max_x - min_x) / 2
|
|
||||||
y_offset = (max_y - min_y) / 2
|
|
||||||
|
|
||||||
match cardinal_point:
|
|
||||||
case "1":
|
|
||||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
|
||||||
case "2":
|
|
||||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
|
||||||
case "3":
|
|
||||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
|
||||||
case "4":
|
|
||||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
|
||||||
case "5":
|
|
||||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
|
||||||
case "6":
|
|
||||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
|
||||||
case "7":
|
|
||||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
|
||||||
case "8":
|
|
||||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
|
||||||
case "9":
|
|
||||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
|
||||||
|
|
||||||
# Create extrusion bmesh
|
|
||||||
bm = bmesh.new()
|
|
||||||
|
|
||||||
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
|
||||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
|
||||||
|
|
||||||
bm.verts.index_update()
|
|
||||||
bm.edges.index_update()
|
|
||||||
|
|
||||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
|
||||||
|
|
||||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
|
||||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
|
||||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
|
|
||||||
|
|
||||||
bm.verts.index_update()
|
|
||||||
bm.edges.index_update()
|
|
||||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
|
||||||
|
|
||||||
# Create bounding box
|
|
||||||
verts = bm.verts
|
|
||||||
i = len(verts)
|
|
||||||
|
|
||||||
min_x = min(v.co.x for v in verts)
|
|
||||||
max_x = max(v.co.x for v in verts)
|
|
||||||
min_y = min(v.co.y for v in verts)
|
|
||||||
max_y = max(v.co.y for v in verts)
|
|
||||||
min_z = min(v.co.z for v in verts)
|
|
||||||
max_z = max(v.co.z for v in verts)
|
|
||||||
|
|
||||||
bbox_verts = [
|
|
||||||
(min_x, min_y, min_z),
|
|
||||||
(max_x, min_y, min_z),
|
|
||||||
(max_x, max_y, min_z),
|
|
||||||
(min_x, max_y, min_z),
|
|
||||||
(min_x, min_y, max_z),
|
|
||||||
(max_x, min_y, max_z),
|
|
||||||
(max_x, max_y, max_z),
|
|
||||||
(min_x, max_y, max_z),
|
|
||||||
]
|
|
||||||
|
|
||||||
bbox_edges = [
|
|
||||||
(0 + i, 3 + i),
|
|
||||||
(3 + i, 7 + i),
|
|
||||||
(7 + i, 4 + i),
|
|
||||||
(4 + i, 0 + i),
|
|
||||||
(0 + i, 1 + i),
|
|
||||||
(3 + i, 2 + i),
|
|
||||||
(7 + i, 6 + i),
|
|
||||||
(4 + i, 5 + i),
|
|
||||||
(1 + i, 2 + i),
|
|
||||||
(2 + i, 6 + i),
|
|
||||||
(6 + i, 5 + i),
|
|
||||||
(5 + i, 1 + i),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Calculate rotation, mouse position, angle and cardinal point
|
|
||||||
# TODO Angle
|
|
||||||
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
|
||||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
|
||||||
data = {}
|
data = {}
|
||||||
|
data["verts"] = [(*v.value_3d,) for v in props.verts]
|
||||||
verts = [tuple(v.co) for v in verts]
|
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
|
||||||
verts.extend(bbox_verts)
|
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
|
||||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
|
||||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
|
||||||
data["verts"] = verts
|
|
||||||
data["edges"] = bbox_edges
|
|
||||||
# data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
|
|
||||||
data["tris"] = tris
|
|
||||||
|
|
||||||
bm.free()
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def draw_product_preview(self, context):
|
def draw_product_preview(self, context):
|
||||||
@@ -1134,25 +834,7 @@ class ProductDecorator:
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Wall
|
product_preview_data = self.get_product_preview_data(context)
|
||||||
if self.relating_type.is_a("IfcWallType"):
|
|
||||||
wall_preview_data = self.get_wall_preview_data(context, self.relating_type)
|
|
||||||
if wall_preview_data:
|
|
||||||
self.draw_batch("LINES", wall_preview_data["verts"], decorator_color, wall_preview_data["edges"])
|
|
||||||
self.draw_batch(
|
|
||||||
"TRIS", wall_preview_data["verts"], transparent_color(decorator_color), wall_preview_data["tris"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mesh type products
|
|
||||||
product_preview_data = self.get_product_preview_data(context, self.relating_type)
|
|
||||||
if product_preview_data:
|
|
||||||
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
|
||||||
self.draw_batch(
|
|
||||||
"TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Profile type products
|
|
||||||
product_preview_data = self.get_profile_preview_data(context, self.relating_type)
|
|
||||||
if product_preview_data:
|
if product_preview_data:
|
||||||
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
||||||
self.draw_batch(
|
self.draw_batch(
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import bmesh
|
import bmesh
|
||||||
from bmesh.types import BMVert, BMFace
|
|
||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api
|
import ifcopenshell.api
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
@@ -169,22 +167,6 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
|
|||||||
tool.Model.update_simple_openings(element)
|
tool.Model.update_simple_openings(element)
|
||||||
|
|
||||||
|
|
||||||
# TODO: move it out to tools
|
|
||||||
def bm_sort_out_geom(
|
|
||||||
geom_data: list[Union[bmesh.types.BMVert, bmesh.types.BMEdge, bmesh.types.BMFace]]
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
geom_dict = {"verts": [], "edges": [], "faces": []}
|
|
||||||
|
|
||||||
for el in geom_data:
|
|
||||||
if isinstance(el, BMVert):
|
|
||||||
geom_dict["verts"].append(el)
|
|
||||||
elif isinstance(el, BMFace):
|
|
||||||
geom_dict["faces"].append(el)
|
|
||||||
else:
|
|
||||||
geom_dict["edges"].append(el)
|
|
||||||
return geom_dict
|
|
||||||
|
|
||||||
|
|
||||||
def bm_mirror(
|
def bm_mirror(
|
||||||
bm: bmesh.types.BMesh,
|
bm: bmesh.types.BMesh,
|
||||||
verts: list[bmesh.types.BMVert],
|
verts: list[bmesh.types.BMVert],
|
||||||
@@ -208,7 +190,7 @@ def bm_mirror(
|
|||||||
for v in verts:
|
for v in verts:
|
||||||
faces.update(v.link_faces)
|
faces.update(v.link_faces)
|
||||||
duplicated = bmesh.ops.duplicate(bm, geom=list(faces))
|
duplicated = bmesh.ops.duplicate(bm, geom=list(faces))
|
||||||
verts = bm_sort_out_geom(duplicated["geom"])["verts"]
|
verts = tool.Model.bm_sort_out_geom(duplicated["geom"])["verts"]
|
||||||
|
|
||||||
bmesh.ops.transform(bm, verts=verts, matrix=matrix, space=Matrix.Identity(4))
|
bmesh.ops.transform(bm, verts=verts, matrix=matrix, space=Matrix.Identity(4))
|
||||||
return verts
|
return verts
|
||||||
@@ -242,7 +224,7 @@ def create_bm_extruded_profile(
|
|||||||
|
|
||||||
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
|
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
|
||||||
extrusion_vector = extrusion_vector * magnitude
|
extrusion_vector = extrusion_vector * magnitude
|
||||||
extruded_verts = bm_sort_out_geom(extruded["geom"])["verts"]
|
extruded_verts = tool.Model.bm_sort_out_geom(extruded["geom"])["verts"]
|
||||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
||||||
|
|
||||||
bmesh.ops.translate(bm, vec=position, verts=new_verts + extruded_verts)
|
bmesh.ops.translate(bm, vec=position, verts=new_verts + extruded_verts)
|
||||||
@@ -304,7 +286,7 @@ def create_bm_door_lining(
|
|||||||
|
|
||||||
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
|
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
|
||||||
extrusion_vector = Vector((0, 1, 0)) * depth
|
extrusion_vector = Vector((0, 1, 0)) * depth
|
||||||
translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)]
|
translate_verts = [v for v in extruded["geom"] if isinstance(v, bmesh.types.BMVert)]
|
||||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
|
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
|
||||||
|
|
||||||
bmesh.ops.translate(bm, vec=position, verts=new_verts + translate_verts)
|
bmesh.ops.translate(bm, vec=position, verts=new_verts + translate_verts)
|
||||||
@@ -572,7 +554,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
update_door_modifier_representation(obj)
|
update_door_modifier_representation(obj)
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
for obj in context.selected_objects:
|
for obj in tool.Blender.get_selected_objects():
|
||||||
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
|
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
|
||||||
continue
|
continue
|
||||||
self.add_door_on_object(obj)
|
self.add_door_on_object(obj)
|
||||||
@@ -610,7 +592,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
props.is_editing = False
|
props.is_editing = False
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
for obj in context.selected_objects:
|
for obj in tool.Blender.get_selected_objects():
|
||||||
self.cancel_editing_door_on_object(obj)
|
self.cancel_editing_door_on_object(obj)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -642,7 +624,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
|
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
for obj in context.selected_objects:
|
for obj in tool.Blender.get_selected_objects():
|
||||||
self.finish_editing_door_on_object(obj)
|
self.finish_editing_door_on_object(obj)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -666,7 +648,7 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
props.is_editing = True
|
props.is_editing = True
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
for obj in context.selected_objects:
|
for obj in tool.Blender.get_selected_objects():
|
||||||
self.edit_door_on_obj(obj)
|
self.edit_door_on_obj(obj)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -686,6 +668,6 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
|
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
for obj in context.selected_objects:
|
for obj in tool.Blender.get_selected_objects():
|
||||||
self.remove_door_on_object(obj)
|
self.remove_door_on_object(obj)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -35,14 +35,440 @@ import bonsai.core.geometry
|
|||||||
import bonsai.core.model as core
|
import bonsai.core.model as core
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from bonsai.bim.ifc import IfcStore
|
from bonsai.bim.ifc import IfcStore
|
||||||
from math import pi, sin, cos, degrees
|
from math import pi, sin, cos, degrees, tan
|
||||||
from mathutils import Vector, Matrix
|
from mathutils import Vector, Matrix, Quaternion
|
||||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||||
from bonsai.bim.module.model.decorator import PolylineDecorator
|
from bonsai.bim.module.model.decorator import PolylineDecorator
|
||||||
|
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||||
from typing import Optional, Union, Literal
|
from typing import Optional, Union, Literal
|
||||||
from lark import Lark, Transformer
|
from lark import Lark, Transformer
|
||||||
|
|
||||||
|
|
||||||
|
def get_wall_preview_data(context, relating_type):
|
||||||
|
def create_bmesh_from_vertices(vertices):
|
||||||
|
bm = bmesh.new()
|
||||||
|
|
||||||
|
new_verts = [bm.verts.new(v) for v in polyline_vertices]
|
||||||
|
if is_closed:
|
||||||
|
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||||
|
new_edges.append(
|
||||||
|
bm.edges.new((new_verts[-1], new_verts[0]))
|
||||||
|
) # Add an edge between the last an first point to make it closed.
|
||||||
|
else:
|
||||||
|
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||||
|
|
||||||
|
bm.verts.index_update()
|
||||||
|
bm.edges.index_update()
|
||||||
|
return bm
|
||||||
|
|
||||||
|
# Get properties from object type
|
||||||
|
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||||
|
if not layers["thickness"]:
|
||||||
|
return
|
||||||
|
thickness = layers["thickness"]
|
||||||
|
model_props = context.scene.BIMModelProperties
|
||||||
|
direction_sense = model_props.direction_sense
|
||||||
|
direction = 1
|
||||||
|
if direction_sense == "NEGATIVE":
|
||||||
|
direction = -1
|
||||||
|
offset_type = model_props.offset_type
|
||||||
|
offset = 0
|
||||||
|
if offset_type == "CENTER":
|
||||||
|
offset = -thickness / 2
|
||||||
|
elif offset_type == "INTERIOR":
|
||||||
|
offset = -thickness
|
||||||
|
|
||||||
|
unit_system = tool.Drawing.get_unit_system()
|
||||||
|
factor = 1
|
||||||
|
if unit_system == "IMPERIAL":
|
||||||
|
factor = 3.048
|
||||||
|
if unit_system == "METRIC":
|
||||||
|
unit_length = context.scene.unit_settings.length_unit
|
||||||
|
if unit_length == "MILLIMETERS":
|
||||||
|
factor = 1000
|
||||||
|
|
||||||
|
# For the model properties, the offset value should just be converted
|
||||||
|
# However, for the wall preview logic that follows, offset and thickness must change direction
|
||||||
|
model_props.offset = offset * factor
|
||||||
|
thickness *= direction
|
||||||
|
offset *= direction
|
||||||
|
|
||||||
|
height = float(model_props.extrusion_depth)
|
||||||
|
rl = float(model_props.rl1)
|
||||||
|
x_angle = float(model_props.x_angle)
|
||||||
|
angle_distortion = height * tan(x_angle)
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data["verts"] = []
|
||||||
|
|
||||||
|
# Verts
|
||||||
|
polyline_vertices = []
|
||||||
|
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||||
|
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||||
|
if len(polyline_points) < 2:
|
||||||
|
data = []
|
||||||
|
return
|
||||||
|
for point in polyline_points:
|
||||||
|
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||||
|
|
||||||
|
is_closed = False
|
||||||
|
if (
|
||||||
|
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||||
|
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||||
|
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||||
|
):
|
||||||
|
is_closed = True
|
||||||
|
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||||
|
|
||||||
|
bm_base = create_bmesh_from_vertices(polyline_vertices)
|
||||||
|
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
||||||
|
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
||||||
|
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
|
||||||
|
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
|
||||||
|
if is_closed:
|
||||||
|
base_vertices.append(base_vertices[0])
|
||||||
|
offset_base_verts.append(offset_base_verts[0])
|
||||||
|
top_vertices.append(top_vertices[0])
|
||||||
|
offset_top_verts.append(offset_top_verts[0])
|
||||||
|
|
||||||
|
if offset_base_verts is not None:
|
||||||
|
for v in base_vertices:
|
||||||
|
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||||
|
|
||||||
|
for v in offset_base_verts[::-1]:
|
||||||
|
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||||
|
|
||||||
|
for v in top_vertices:
|
||||||
|
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||||
|
|
||||||
|
for v in offset_top_verts[::-1]:
|
||||||
|
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||||
|
|
||||||
|
bm_base.free()
|
||||||
|
|
||||||
|
# Edges and Tris
|
||||||
|
points = []
|
||||||
|
side_edges_1 = []
|
||||||
|
side_edges_2 = []
|
||||||
|
base_edges = []
|
||||||
|
|
||||||
|
for i in range(len(data["verts"])):
|
||||||
|
points.append(Vector(data["verts"][i]))
|
||||||
|
|
||||||
|
n = len(points) // 2
|
||||||
|
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
||||||
|
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
||||||
|
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
||||||
|
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
||||||
|
side_edges_1.extend(bottom_side_1)
|
||||||
|
side_edges_2.extend(bottom_side_2)
|
||||||
|
base_edges.extend(bottom_loop)
|
||||||
|
|
||||||
|
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
||||||
|
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
||||||
|
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
||||||
|
side_edges_1.extend(upper_side_1)
|
||||||
|
side_edges_2.extend(upper_side_2)
|
||||||
|
base_edges.extend(upper_loop)
|
||||||
|
|
||||||
|
loops = [side_edges_1, side_edges_2, base_edges]
|
||||||
|
|
||||||
|
data["edges"] = []
|
||||||
|
data["tris"] = []
|
||||||
|
for i, group in enumerate(loops):
|
||||||
|
bm = bmesh.new()
|
||||||
|
|
||||||
|
new_verts = [bm.verts.new(v) for v in points]
|
||||||
|
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
||||||
|
|
||||||
|
bm.verts.index_update()
|
||||||
|
bm.edges.index_update()
|
||||||
|
|
||||||
|
if i == 2:
|
||||||
|
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||||
|
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
||||||
|
|
||||||
|
bm.verts.index_update()
|
||||||
|
bm.edges.index_update()
|
||||||
|
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||||
|
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
||||||
|
data["edges"].extend(edges)
|
||||||
|
data["tris"].extend(tris)
|
||||||
|
|
||||||
|
data["edges"] = list(set(tuple(e) for e in data["edges"]))
|
||||||
|
data["tris"] = list(set(tuple(t) for t in data["tris"]))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_vertical_profile_preview_data(context, relating_type):
|
||||||
|
material = ifcopenshell.util.element.get_material(relating_type)
|
||||||
|
try:
|
||||||
|
profile = material.MaterialProfiles[0].Profile
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
model_props = context.scene.BIMModelProperties
|
||||||
|
extrusion_depth = model_props.extrusion_depth
|
||||||
|
cardinal_point = model_props.cardinal_point
|
||||||
|
rot_mat = Quaternion()
|
||||||
|
if relating_type.is_a("IfcBeamType"):
|
||||||
|
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
|
||||||
|
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
|
||||||
|
rot_mat = y_rot @ z_rot
|
||||||
|
# Get profile data
|
||||||
|
settings = ifcopenshell.geom.settings()
|
||||||
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||||
|
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||||
|
|
||||||
|
verts = shape.verts
|
||||||
|
if not verts:
|
||||||
|
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
|
||||||
|
|
||||||
|
edges = shape.edges
|
||||||
|
|
||||||
|
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||||
|
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||||
|
|
||||||
|
# Create offsets based on cardinal point
|
||||||
|
min_x = min(v[0] for v in grouped_verts)
|
||||||
|
max_x = max(v[0] for v in grouped_verts)
|
||||||
|
min_y = min(v[1] for v in grouped_verts)
|
||||||
|
max_y = max(v[1] for v in grouped_verts)
|
||||||
|
|
||||||
|
x_offset = (max_x - min_x) / 2
|
||||||
|
y_offset = (max_y - min_y) / 2
|
||||||
|
|
||||||
|
match cardinal_point:
|
||||||
|
case "1":
|
||||||
|
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "2":
|
||||||
|
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "3":
|
||||||
|
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "4":
|
||||||
|
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||||
|
case "5":
|
||||||
|
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||||
|
case "6":
|
||||||
|
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||||
|
case "7":
|
||||||
|
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "8":
|
||||||
|
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "9":
|
||||||
|
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||||
|
|
||||||
|
# Create extrusion bmesh
|
||||||
|
bm = bmesh.new()
|
||||||
|
|
||||||
|
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||||
|
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
||||||
|
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
||||||
|
|
||||||
|
bm.verts.index_update()
|
||||||
|
bm.edges.index_update()
|
||||||
|
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||||
|
|
||||||
|
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||||
|
|
||||||
|
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
|
||||||
|
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||||
|
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
|
||||||
|
|
||||||
|
bm.verts.index_update()
|
||||||
|
bm.edges.index_update()
|
||||||
|
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||||
|
|
||||||
|
# Calculate rotation, mouse position, angle and cardinal point
|
||||||
|
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
||||||
|
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
verts = [tuple(v.co) for v in bm.verts]
|
||||||
|
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||||
|
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||||
|
min_z = min(v.co.z for v in bm.verts)
|
||||||
|
max_z = max(v.co.z for v in bm.verts)
|
||||||
|
# Add axis verts
|
||||||
|
verts.append(tuple(mouse_point))
|
||||||
|
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
|
||||||
|
# Add only profile edges
|
||||||
|
edges = []
|
||||||
|
for edge in bm.edges:
|
||||||
|
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
|
||||||
|
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
|
||||||
|
):
|
||||||
|
edges.append(edge)
|
||||||
|
# Add axis edge
|
||||||
|
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
|
||||||
|
edges.append((len(verts) - 1, len(verts) - 2))
|
||||||
|
data["verts"] = verts
|
||||||
|
data["edges"] = edges
|
||||||
|
data["tris"] = tris
|
||||||
|
|
||||||
|
bm.free()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_horizontal_profile_preview_data(context, relating_type):
|
||||||
|
material = ifcopenshell.util.element.get_material(relating_type)
|
||||||
|
try:
|
||||||
|
profile_curve = material.MaterialProfiles[0].Profile
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
model_props = context.scene.BIMModelProperties
|
||||||
|
cardinal_point = model_props.cardinal_point
|
||||||
|
|
||||||
|
polyline_verts = []
|
||||||
|
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||||
|
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||||
|
if len(polyline_points) < 2:
|
||||||
|
return
|
||||||
|
for point in polyline_points:
|
||||||
|
polyline_verts.append(Vector((point.x, point.y, point.z)))
|
||||||
|
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
|
||||||
|
|
||||||
|
# Get profile shape
|
||||||
|
settings = ifcopenshell.geom.settings()
|
||||||
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||||
|
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
|
||||||
|
|
||||||
|
verts = shape.verts
|
||||||
|
if not verts:
|
||||||
|
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
|
||||||
|
|
||||||
|
edges = shape.edges
|
||||||
|
|
||||||
|
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||||
|
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||||
|
|
||||||
|
# Create offsets based on cardinal point
|
||||||
|
min_x = min(v[0] for v in grouped_verts)
|
||||||
|
max_x = max(v[0] for v in grouped_verts)
|
||||||
|
min_y = min(v[1] for v in grouped_verts)
|
||||||
|
max_y = max(v[1] for v in grouped_verts)
|
||||||
|
|
||||||
|
x_offset = (max_x - min_x) / 2
|
||||||
|
y_offset = (max_y - min_y) / 2
|
||||||
|
|
||||||
|
match cardinal_point:
|
||||||
|
case "1":
|
||||||
|
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "2":
|
||||||
|
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "3":
|
||||||
|
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "4":
|
||||||
|
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||||
|
case "5":
|
||||||
|
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||||
|
case "6":
|
||||||
|
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||||
|
case "7":
|
||||||
|
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "8":
|
||||||
|
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||||
|
case "9":
|
||||||
|
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||||
|
|
||||||
|
# Create profile curve
|
||||||
|
scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0))
|
||||||
|
grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts]
|
||||||
|
profile_curve = bpy.data.curves.new("Profile", type="CURVE")
|
||||||
|
profile_curve.dimensions = "2D"
|
||||||
|
profile_curve.splines.new("POLY")
|
||||||
|
profile_curve.splines[0].points.add(len(grouped_verts))
|
||||||
|
|
||||||
|
for i, point in enumerate(profile_curve.splines[0].points):
|
||||||
|
if i == len(grouped_verts): # Close curve
|
||||||
|
point.co = Vector((*grouped_verts[0], 0))
|
||||||
|
continue
|
||||||
|
point.co = Vector((*grouped_verts[i], 0))
|
||||||
|
profile_obj = bpy.data.objects.new("Profile", profile_curve)
|
||||||
|
|
||||||
|
# Create path curve with profile object as bevel
|
||||||
|
path_curve = bpy.data.curves.new("Polyline", type="CURVE")
|
||||||
|
path_curve.dimensions = "2D"
|
||||||
|
path_curve.splines.new("POLY")
|
||||||
|
path_curve.splines[0].points.add(len(polyline_verts) - 1)
|
||||||
|
for i, point in enumerate(path_curve.splines[0].points):
|
||||||
|
point.co = Vector((*polyline_verts[i], 0))
|
||||||
|
path_curve.splines[0].use_smooth = False
|
||||||
|
path_curve.bevel_mode = "OBJECT"
|
||||||
|
path_curve.bevel_object = profile_obj
|
||||||
|
|
||||||
|
# Convert path curve to mesh
|
||||||
|
# This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
|
||||||
|
path_obj = bpy.data.objects.new("Preview", path_curve)
|
||||||
|
context.scene.collection.objects.link(path_obj)
|
||||||
|
bpy.context.view_layer.objects.active = path_obj
|
||||||
|
dg = context.evaluated_depsgraph_get()
|
||||||
|
path_obj = path_obj.evaluated_get(dg)
|
||||||
|
me = path_obj.to_mesh()
|
||||||
|
|
||||||
|
# Create bmesh from path mesh
|
||||||
|
bm = bmesh.new()
|
||||||
|
new_verts = [bm.verts.new(v.co) for v in me.vertices]
|
||||||
|
index = [[v for v in edge.vertices] for edge in me.edges]
|
||||||
|
new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index]
|
||||||
|
for face in me.polygons:
|
||||||
|
verts = [new_verts[i] for i in face.vertices]
|
||||||
|
bm.faces.new(verts)
|
||||||
|
bm.verts.index_update()
|
||||||
|
bm.edges.index_update()
|
||||||
|
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||||
|
|
||||||
|
bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True)
|
||||||
|
bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True)
|
||||||
|
try:
|
||||||
|
bpy.data.curves.remove(profile_obj.data, do_unlink=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
bpy.data.curves.remove(path_obj.data, do_unlink=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data["verts"] = [tuple(v.co) for v in bm.verts]
|
||||||
|
data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
|
||||||
|
data["tris"] = tris
|
||||||
|
|
||||||
|
bm.free()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_product_preview_data(context, relating_type):
|
||||||
|
model_props = context.scene.BIMModelProperties
|
||||||
|
if relating_type.is_a("IfcDoorType"):
|
||||||
|
rl = float(model_props.rl1)
|
||||||
|
elif relating_type.is_a("IfcWindowType"):
|
||||||
|
rl = float(model_props.rl2)
|
||||||
|
else:
|
||||||
|
rl = 0
|
||||||
|
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
||||||
|
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
|
||||||
|
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||||
|
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
||||||
|
snap_element = tool.Ifc.get_entity(snap_obj)
|
||||||
|
rot_mat = Quaternion()
|
||||||
|
if snap_element and snap_element.is_a("IfcWall"):
|
||||||
|
rot_mat = snap_obj.matrix_world.to_quaternion()
|
||||||
|
|
||||||
|
obj_type = tool.Ifc.get_object(relating_type)
|
||||||
|
if obj_type.data:
|
||||||
|
data = ItemDecorator.get_obj_data(obj_type)
|
||||||
|
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
|
||||||
|
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class PolylineOperator:
|
class PolylineOperator:
|
||||||
# TODO Fill doc strings
|
# TODO Fill doc strings
|
||||||
""" """
|
""" """
|
||||||
@@ -387,6 +813,34 @@ class PolylineOperator:
|
|||||||
tool.Polyline.remove_last_polyline_point()
|
tool.Polyline.remove_last_polyline_point()
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
|
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_isntance):
|
||||||
|
if tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() not in {"IfcColumnType"}:
|
||||||
|
data = get_horizontal_profile_preview_data(context, relating_type)
|
||||||
|
elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() in {"IfcColumnType"}:
|
||||||
|
data = get_vertical_profile_preview_data(context, relating_type)
|
||||||
|
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||||
|
data = get_wall_preview_data(context, relating_type)
|
||||||
|
else:
|
||||||
|
data = get_product_preview_data(context, relating_type)
|
||||||
|
|
||||||
|
# Update properties so it can be used by the decorator
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
props = context.scene.BIMProductPreviewProperties
|
||||||
|
props.verts.clear()
|
||||||
|
props.edges.clear()
|
||||||
|
props.tris.clear()
|
||||||
|
|
||||||
|
for vert in data["verts"]:
|
||||||
|
v = props.verts.add()
|
||||||
|
v.value_3d = vert
|
||||||
|
for edge in data["edges"]:
|
||||||
|
e = props.edges.add()
|
||||||
|
e.value_2d = edge
|
||||||
|
for tri in data["tris"]:
|
||||||
|
t = props.tris.add()
|
||||||
|
t.value_3d = tri
|
||||||
|
|
||||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> Union[set[str], None]:
|
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> Union[set[str], None]:
|
||||||
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator):
|
|||||||
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
|
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
|
||||||
self.create_occurrence(context, event)
|
self.create_occurrence(context, event)
|
||||||
|
|
||||||
|
self.get_product_preview_data(context, self.relating_type)
|
||||||
|
|
||||||
cancel = self.handle_cancelation(context, event)
|
cancel = self.handle_cancelation(context, event)
|
||||||
if cancel is not None:
|
if cancel is not None:
|
||||||
ProductDecorator.uninstall()
|
ProductDecorator.uninstall()
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ import bonsai.core.type
|
|||||||
import bonsai.core.geometry
|
import bonsai.core.geometry
|
||||||
import bonsai.core.material
|
import bonsai.core.material
|
||||||
import bonsai.core.root
|
import bonsai.core.root
|
||||||
from math import pi, degrees, inf
|
from math import pi, degrees, inf, atan2
|
||||||
from mathutils import Vector, Matrix, Quaternion
|
from mathutils import Vector, Matrix, Quaternion
|
||||||
from bonsai.bim.module.geometry.helper import Helper
|
from bonsai.bim.module.geometry.helper import Helper
|
||||||
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
||||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||||
|
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||||
|
from typing import Union, Any
|
||||||
|
|
||||||
|
|
||||||
class DumbProfileGenerator:
|
class DumbProfileGenerator:
|
||||||
@@ -44,7 +46,7 @@ class DumbProfileGenerator:
|
|||||||
self.relating_type = relating_type
|
self.relating_type = relating_type
|
||||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
|
||||||
def generate(self):
|
def generate(self, insertion_type="CURSOR"):
|
||||||
self.file = tool.Ifc.get()
|
self.file = tool.Ifc.get()
|
||||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
material = ifcopenshell.util.element.get_material(self.relating_type)
|
material = ifcopenshell.util.element.get_material(self.relating_type)
|
||||||
@@ -67,7 +69,28 @@ class DumbProfileGenerator:
|
|||||||
self.rotation = 0
|
self.rotation = 0
|
||||||
self.location = Vector((0, 0, 0))
|
self.location = Vector((0, 0, 0))
|
||||||
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
|
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
|
||||||
return self.derive_from_cursor()
|
if insertion_type == "POLYLINE":
|
||||||
|
return self.derive_from_polyline()
|
||||||
|
elif insertion_type == "CURSOR":
|
||||||
|
return self.derive_from_cursor()
|
||||||
|
|
||||||
|
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
|
||||||
|
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
|
||||||
|
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||||
|
is_polyline_closed = False
|
||||||
|
if len(polyline_points) > 3:
|
||||||
|
first_vec = Vector((polyline_points[0].x, polyline_points[0].y, polyline_points[0].z))
|
||||||
|
last_vec = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z))
|
||||||
|
if first_vec == last_vec:
|
||||||
|
is_polyline_closed = True
|
||||||
|
|
||||||
|
profiles = []
|
||||||
|
for i in range(len(polyline_points) - 1):
|
||||||
|
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
|
||||||
|
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
|
||||||
|
coords = (vec1, vec2)
|
||||||
|
profiles.append(self.create_profile_from_2_points(coords))
|
||||||
|
return profiles, is_polyline_closed
|
||||||
|
|
||||||
def derive_from_cursor(self):
|
def derive_from_cursor(self):
|
||||||
self.location = bpy.context.scene.cursor.location
|
self.location = bpy.context.scene.cursor.location
|
||||||
@@ -82,10 +105,13 @@ class DumbProfileGenerator:
|
|||||||
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
|
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
|
||||||
|
|
||||||
matrix_world = Matrix()
|
matrix_world = Matrix()
|
||||||
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"] or self.relating_type.is_a(
|
if self.relating_type.is_a() in ["IfcBeamType", "IfcCoveringType", "IfcMemberType"] or self.relating_type.is_a(
|
||||||
"IfcFlowSegmentType"
|
"IfcFlowSegmentType"
|
||||||
):
|
):
|
||||||
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
|
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
|
||||||
|
|
||||||
|
matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
|
||||||
|
|
||||||
matrix_world.translation = self.location
|
matrix_world.translation = self.location
|
||||||
if self.container_obj:
|
if self.container_obj:
|
||||||
matrix_world.translation.z = self.container_obj.location.z
|
matrix_world.translation.z = self.container_obj.location.z
|
||||||
@@ -146,6 +172,25 @@ class DumbProfileGenerator:
|
|||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
|
||||||
|
direction = coords[1] - coords[0]
|
||||||
|
length = direction.length
|
||||||
|
if round(length, 4) < 0.1:
|
||||||
|
return
|
||||||
|
data = {"coords": coords}
|
||||||
|
|
||||||
|
self.depth = length
|
||||||
|
self.rotation = atan2(direction[1], direction[0])
|
||||||
|
if should_round:
|
||||||
|
# Round to nearest 50mm (yes, metric for now)
|
||||||
|
self.length = 0.05 * round(length / 0.05)
|
||||||
|
# Round to nearest 5 degrees
|
||||||
|
nearest_degree = (math.pi / 180) * 5
|
||||||
|
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
|
||||||
|
self.location = coords[0]
|
||||||
|
data["obj"] = self.create_profile()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class DumbProfileRegenerator:
|
class DumbProfileRegenerator:
|
||||||
def regenerate_from_profile_def(self, profile):
|
def regenerate_from_profile_def(self, profile):
|
||||||
@@ -1064,3 +1109,114 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
joiner = DumbProfileJoiner()
|
joiner = DumbProfileJoiner()
|
||||||
joiner.set_depth(obj, depth)
|
joiner.set_depth(obj, depth)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
|
||||||
|
bl_idname = "bim.draw_polyline_profile"
|
||||||
|
bl_label = "Draw Polyline Profile"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.space_data.type == "VIEW_3D"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.relating_type = None
|
||||||
|
props = bpy.context.scene.BIMModelProperties
|
||||||
|
relating_type_id = props.relating_type_id
|
||||||
|
if relating_type_id:
|
||||||
|
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||||
|
|
||||||
|
def create_profiles_from_polyline(self, context: bpy.types.Context) -> Union[set[str], None]:
|
||||||
|
if not self.relating_type:
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
model_props = context.scene.BIMModelProperties
|
||||||
|
direction_sense = model_props.direction_sense
|
||||||
|
offset = model_props.offset
|
||||||
|
|
||||||
|
profiles, is_polyline_closed = DumbProfileGenerator(self.relating_type).generate("POLYLINE")
|
||||||
|
if profiles:
|
||||||
|
if is_polyline_closed:
|
||||||
|
for profile1, profile2 in zip(profiles, profiles[1:] + [profiles[0]]):
|
||||||
|
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||||
|
else:
|
||||||
|
for profile1, profile2 in zip(profiles[:-1], profiles[1:]):
|
||||||
|
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if not self.relating_type:
|
||||||
|
self.report({"WARNING"}, "You need to select a profile type.")
|
||||||
|
PolylineDecorator.uninstall()
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
|
self.handle_lock_axis(context, event) # Must come before "PASS_TRHOUGH"
|
||||||
|
|
||||||
|
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
|
||||||
|
self.handle_mouse_move(context, event)
|
||||||
|
return {"PASS_THROUGH"}
|
||||||
|
|
||||||
|
# Wall axis settings
|
||||||
|
if event.value == "RELEASE" and event.type == "F":
|
||||||
|
direction_sense = context.scene.BIMModelProperties.direction_sense
|
||||||
|
context.scene.BIMModelProperties.direction_sense = (
|
||||||
|
"NEGATIVE" if direction_sense == "POSITIVE" else "POSITIVE"
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.value == "RELEASE" and event.type == "O":
|
||||||
|
offset_type = context.scene.BIMModelProperties.offset_type
|
||||||
|
items = ["EXTERIOR", "CENTER", "INTERIOR"]
|
||||||
|
index = items.index(offset_type)
|
||||||
|
size = len(items)
|
||||||
|
context.scene.BIMModelProperties.offset_type = items[((index + 1) % size)]
|
||||||
|
|
||||||
|
props = bpy.context.scene.BIMModelProperties
|
||||||
|
wall_config = f"""Direction: {props.direction_sense}
|
||||||
|
Offset Type: {props.offset_type}
|
||||||
|
Offset Value: {props.offset}
|
||||||
|
"""
|
||||||
|
self.handle_instructions(context, wall_config)
|
||||||
|
|
||||||
|
self.handle_mouse_move(context, event, should_round=True)
|
||||||
|
|
||||||
|
self.choose_axis(event)
|
||||||
|
|
||||||
|
self.handle_snap_selection(context, event)
|
||||||
|
|
||||||
|
if (
|
||||||
|
not self.tool_state.is_input_on
|
||||||
|
and event.value == "RELEASE"
|
||||||
|
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
|
||||||
|
):
|
||||||
|
self.create_profiles_from_polyline(context)
|
||||||
|
context.workspace.status_text_set(text=None)
|
||||||
|
ProductDecorator.uninstall()
|
||||||
|
PolylineDecorator.uninstall()
|
||||||
|
tool.Polyline.clear_polyline()
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
self.handle_keyboard_input(context, event)
|
||||||
|
|
||||||
|
self.handle_inserting_polyline(context, event)
|
||||||
|
|
||||||
|
self.get_product_preview_data(context, self.relating_type)
|
||||||
|
|
||||||
|
cancel = self.handle_cancelation(context, event)
|
||||||
|
if cancel is not None:
|
||||||
|
ProductDecorator.uninstall()
|
||||||
|
return cancel
|
||||||
|
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
super().invoke(context, event)
|
||||||
|
ProductDecorator.install(context)
|
||||||
|
self.tool_state.use_default_container = True
|
||||||
|
self.tool_state.plane_method = "XY"
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ def update_search_name(self, context):
|
|||||||
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
|
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
|
||||||
|
|
||||||
|
|
||||||
|
def update_x_angle(self, context):
|
||||||
|
angle_deg = math.degrees(self.x_angle)
|
||||||
|
if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5):
|
||||||
|
self.x_angle = 0
|
||||||
|
|
||||||
|
|
||||||
class BIMModelProperties(PropertyGroup):
|
class BIMModelProperties(PropertyGroup):
|
||||||
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
|
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
|
||||||
relating_type_id: bpy.props.EnumProperty(
|
relating_type_id: bpy.props.EnumProperty(
|
||||||
@@ -183,8 +189,10 @@ class BIMModelProperties(PropertyGroup):
|
|||||||
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
|
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
|
||||||
# Used for plan calculation points such as in room generation
|
# Used for plan calculation points such as in room generation
|
||||||
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
|
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
|
||||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89)
|
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
|
||||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1,update=update_type_page)
|
x_angle: bpy.props.FloatProperty(
|
||||||
|
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
|
||||||
|
)
|
||||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||||
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
|
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
|
||||||
direction_sense: bpy.props.EnumProperty(
|
direction_sense: bpy.props.EnumProperty(
|
||||||
@@ -842,3 +850,14 @@ class BIMPolylineProperties(PropertyGroup):
|
|||||||
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
|
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
|
||||||
insertion_polyline: bpy.props.CollectionProperty(type=Polyline)
|
insertion_polyline: bpy.props.CollectionProperty(type=Polyline)
|
||||||
measurement_polyline: bpy.props.CollectionProperty(type=Polyline)
|
measurement_polyline: bpy.props.CollectionProperty(type=Polyline)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductPreviewItem(PropertyGroup):
|
||||||
|
value_3d: bpy.props.FloatVectorProperty()
|
||||||
|
value_2d: bpy.props.FloatVectorProperty(size=2)
|
||||||
|
|
||||||
|
|
||||||
|
class BIMProductPreviewProperties(PropertyGroup):
|
||||||
|
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||||
|
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||||
|
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import ifcopenshell.util.unit
|
|||||||
import bonsai.core.root
|
import bonsai.core.root
|
||||||
import bonsai.core.geometry
|
import bonsai.core.geometry
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from bonsai.bim.module.model.door import bm_sort_out_geom
|
|
||||||
from bonsai.bim.module.model.data import RailingData, refresh
|
from bonsai.bim.module.model.data import RailingData, refresh
|
||||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ def bm_split_edge_at_offset(edge: bmesh.types.BMEdge, offset: float) -> dict[str
|
|||||||
|
|
||||||
split_output_0 = bmesh.utils.edge_split(edge, v0, offset / edge_len)
|
split_output_0 = bmesh.utils.edge_split(edge, v0, offset / edge_len)
|
||||||
split_output_1 = bmesh.utils.edge_split(edge, v1, offset / (edge_len - offset))
|
split_output_1 = bmesh.utils.edge_split(edge, v1, offset / (edge_len - offset))
|
||||||
new_geometry = bm_sort_out_geom(split_output_0 + split_output_1)
|
new_geometry = tool.Model.bm_sort_out_geom(split_output_0 + split_output_1)
|
||||||
return new_geometry
|
return new_geometry
|
||||||
|
|
||||||
|
|
||||||
@@ -175,11 +174,11 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
|
|||||||
ortho_vector = edge_dir.cross(V_(0, 0, 1))
|
ortho_vector = edge_dir.cross(V_(0, 0, 1))
|
||||||
|
|
||||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
||||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
|
||||||
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
|
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
|
||||||
|
|
||||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
||||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
|
||||||
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
|
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
|
||||||
|
|
||||||
# dissolve middle edge
|
# dissolve middle edge
|
||||||
@@ -187,7 +186,7 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
|
|||||||
|
|
||||||
# height
|
# height
|
||||||
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
|
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
|
||||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
|
||||||
extrusion_vector = Vector((0, 0, 1)) * height
|
extrusion_vector = Vector((0, 0, 1)) * height
|
||||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import ifcopenshell.util.unit
|
|||||||
import bonsai.core.root
|
import bonsai.core.root
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from bonsai.bim.helper import convert_property_group_from_si
|
from bonsai.bim.helper import convert_property_group_from_si
|
||||||
from bonsai.bim.module.model.door import bm_sort_out_geom
|
|
||||||
from bonsai.bim.module.model.data import RoofData, refresh
|
from bonsai.bim.module.model.data import RoofData, refresh
|
||||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||||
|
|
||||||
@@ -327,7 +326,7 @@ def generate_hiped_roof_bmesh(
|
|||||||
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
|
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
|
||||||
|
|
||||||
# add roof thickness
|
# add roof thickness
|
||||||
extrusion_geom = bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
|
extrusion_geom = tool.Model.bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
|
||||||
extruded_edges = extrusion_geom["edges"]
|
extruded_edges = extrusion_geom["edges"]
|
||||||
extruded_verts = extrusion_geom["verts"]
|
extruded_verts = extrusion_geom["verts"]
|
||||||
rafter_edge_angle = pi / 2 - rafter_edge_angle
|
rafter_edge_angle = pi / 2 - rafter_edge_angle
|
||||||
|
|||||||
@@ -421,6 +421,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator):
|
|||||||
|
|
||||||
self.handle_inserting_polyline(context, event)
|
self.handle_inserting_polyline(context, event)
|
||||||
|
|
||||||
|
self.get_product_preview_data(context, self.relating_type)
|
||||||
|
|
||||||
cancel = self.handle_cancelation(context, event)
|
cancel = self.handle_cancelation(context, event)
|
||||||
if cancel is not None:
|
if cancel is not None:
|
||||||
ProductDecorator.uninstall()
|
ProductDecorator.uninstall()
|
||||||
|
|||||||
@@ -118,10 +118,6 @@ class BimTool(WorkSpaceTool):
|
|||||||
EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||||
else:
|
else:
|
||||||
CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||||
# Show some UI for spatial elements that are unselectable by default.
|
|
||||||
if active_ifc_object:
|
|
||||||
EditObjectUI.layout = layout # Prevent .draw_modes from using old layout and crash.
|
|
||||||
EditObjectUI.draw_modes(context)
|
|
||||||
|
|
||||||
|
|
||||||
class WallTool(BimTool):
|
class WallTool(BimTool):
|
||||||
@@ -955,6 +951,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
def hotkey_S_A(self):
|
def hotkey_S_A(self):
|
||||||
props = bpy.context.scene.BIMModelProperties
|
props = bpy.context.scene.BIMModelProperties
|
||||||
relating_type_id = AuthoringData.data["relating_type_id_current"]
|
relating_type_id = AuthoringData.data["relating_type_id_current"]
|
||||||
|
relating_type_class = AuthoringData.data["ifc_class_current"]
|
||||||
if relating_type_id is None:
|
if relating_type_id is None:
|
||||||
self.report({"ERROR"}, "No relating type selected")
|
self.report({"ERROR"}, "No relating type selected")
|
||||||
return
|
return
|
||||||
@@ -994,6 +991,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
|
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
|
||||||
):
|
):
|
||||||
bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
|
bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
|
||||||
|
elif (
|
||||||
|
relating_type_id
|
||||||
|
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "PROFILE"
|
||||||
|
and relating_type_class not in {"IfcColumnType"}
|
||||||
|
):
|
||||||
|
bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT")
|
||||||
else:
|
else:
|
||||||
bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
|
bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
|
||||||
|
|
||||||
|
|||||||
@@ -98,12 +98,6 @@ class ExecuteIfcPatch(bpy.types.Operator):
|
|||||||
|
|
||||||
if props.should_load_from_memory and tool.Ifc.get():
|
if props.should_load_from_memory and tool.Ifc.get():
|
||||||
args["file"] = tool.Ifc.get()
|
args["file"] = tool.Ifc.get()
|
||||||
if ifcpatch.get_patch_input_argument_use(recipe_name) == "REQUIRED":
|
|
||||||
self.report(
|
|
||||||
{"ERROR"},
|
|
||||||
f"The recipe '{recipe_name}' is not currently supported if file is loaded from memory.",
|
|
||||||
)
|
|
||||||
return {"CANCELLED"}
|
|
||||||
else:
|
else:
|
||||||
args["input"] = cast(str, props.ifc_patch_input)
|
args["input"] = cast(str, props.ifc_patch_input)
|
||||||
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
|
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ classes = (
|
|||||||
ui.BIM_PT_object_psets,
|
ui.BIM_PT_object_psets,
|
||||||
ui.BIM_PT_object_qtos,
|
ui.BIM_PT_object_qtos,
|
||||||
ui.BIM_PT_material_psets,
|
ui.BIM_PT_material_psets,
|
||||||
ui.BIM_PT_material_set_psets,
|
|
||||||
ui.BIM_PT_material_set_item_psets,
|
ui.BIM_PT_material_set_item_psets,
|
||||||
ui.BIM_PT_task_qtos,
|
ui.BIM_PT_task_qtos,
|
||||||
ui.BIM_PT_resource_qtos,
|
ui.BIM_PT_resource_qtos,
|
||||||
@@ -66,7 +65,6 @@ classes = (
|
|||||||
def register():
|
def register():
|
||||||
bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||||
bpy.types.Scene.MaterialPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
bpy.types.Scene.MaterialPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||||
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
|
||||||
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||||
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||||
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||||
@@ -82,7 +80,6 @@ def register():
|
|||||||
def unregister():
|
def unregister():
|
||||||
del bpy.types.Object.PsetProperties
|
del bpy.types.Object.PsetProperties
|
||||||
del bpy.types.Scene.MaterialPsetProperties
|
del bpy.types.Scene.MaterialPsetProperties
|
||||||
del bpy.types.Object.MaterialSetPsetProperties
|
|
||||||
del bpy.types.Object.MaterialSetItemPsetProperties
|
del bpy.types.Object.MaterialSetItemPsetProperties
|
||||||
del bpy.types.Scene.TaskPsetProperties
|
del bpy.types.Scene.TaskPsetProperties
|
||||||
del bpy.types.Scene.ResourcePsetProperties
|
del bpy.types.Scene.ResourcePsetProperties
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ def refresh():
|
|||||||
ObjectPsetsData.is_loaded = False
|
ObjectPsetsData.is_loaded = False
|
||||||
ObjectQtosData.is_loaded = False
|
ObjectQtosData.is_loaded = False
|
||||||
MaterialPsetsData.is_loaded = False
|
MaterialPsetsData.is_loaded = False
|
||||||
MaterialSetPsetsData.is_loaded = False
|
|
||||||
MaterialSetItemPsetsData.is_loaded = False
|
MaterialSetItemPsetsData.is_loaded = False
|
||||||
TaskQtosData.is_loaded = False
|
TaskQtosData.is_loaded = False
|
||||||
ResourceQtosData.is_loaded = False
|
ResourceQtosData.is_loaded = False
|
||||||
@@ -185,7 +184,7 @@ class MaterialPsetsData(Data):
|
|||||||
if material.ifc_definition_id:
|
if material.ifc_definition_id:
|
||||||
material = tool.Ifc.get().by_id(material.ifc_definition_id)
|
material = tool.Ifc.get().by_id(material.ifc_definition_id)
|
||||||
category = getattr(material, "Category", None) or None
|
category = getattr(material, "Category", None) or None
|
||||||
psets = bonsai.bim.schema.ifc.psetqto.get_applicable("IfcMaterial", category, pset_only=True)
|
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(props.material_type, category, pset_only=True)
|
||||||
psetnames = cls.format_pset_enum(psets)
|
psetnames = cls.format_pset_enum(psets)
|
||||||
assigned_names = ifcopenshell.util.element.get_psets(
|
assigned_names = ifcopenshell.util.element.get_psets(
|
||||||
material, psets_only=True, should_inherit=False
|
material, psets_only=True, should_inherit=False
|
||||||
@@ -194,22 +193,6 @@ class MaterialPsetsData(Data):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
class MaterialSetPsetsData(Data):
|
|
||||||
data = {}
|
|
||||||
is_loaded = False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load(cls):
|
|
||||||
psets = {}
|
|
||||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
|
||||||
if element:
|
|
||||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
|
||||||
if material and "Set" in material.is_a():
|
|
||||||
psets = cls.psetqtos(material)
|
|
||||||
cls.data = {"psets": psets}
|
|
||||||
cls.is_loaded = True
|
|
||||||
|
|
||||||
|
|
||||||
class MaterialSetItemPsetsData(Data):
|
class MaterialSetItemPsetsData(Data):
|
||||||
data = {}
|
data = {}
|
||||||
is_loaded = False
|
is_loaded = False
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from bonsai.bim.module.pset.data import (
|
|||||||
ObjectPsetsData,
|
ObjectPsetsData,
|
||||||
ObjectQtosData,
|
ObjectQtosData,
|
||||||
MaterialPsetsData,
|
MaterialPsetsData,
|
||||||
MaterialSetPsetsData,
|
|
||||||
MaterialSetItemPsetsData,
|
MaterialSetItemPsetsData,
|
||||||
TaskQtosData,
|
TaskQtosData,
|
||||||
ResourceQtosData,
|
ResourceQtosData,
|
||||||
@@ -88,7 +87,7 @@ def draw_enumerated_property(
|
|||||||
|
|
||||||
|
|
||||||
def get_active_pset_obj_name(context: bpy.types.Context, obj_type: tool.Ifc.OBJECT_TYPE) -> str:
|
def get_active_pset_obj_name(context: bpy.types.Context, obj_type: tool.Ifc.OBJECT_TYPE) -> str:
|
||||||
if obj_type in ("Object", "MaterialSet", "MaterialSetItem"):
|
if obj_type in ("Object", "MaterialSetItem"):
|
||||||
return context.active_object.name
|
return context.active_object.name
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -197,7 +196,9 @@ def draw_psetqto_ui(
|
|||||||
row = box.row(align=True)
|
row = box.row(align=True)
|
||||||
row.scale_y = 0.8
|
row.scale_y = 0.8
|
||||||
row.label(text=prop["Name"])
|
row.label(text=prop["Name"])
|
||||||
op = row.operator("bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False)
|
op = row.operator(
|
||||||
|
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
|
||||||
|
)
|
||||||
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
|
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
|
||||||
# calculate sum of all selected objects
|
# calculate sum of all selected objects
|
||||||
if active_operator:
|
if active_operator:
|
||||||
@@ -400,10 +401,8 @@ class BIM_PT_material_psets(Panel):
|
|||||||
if not ifc_file or ifc_file.schema == "IFC2X3":
|
if not ifc_file or ifc_file.schema == "IFC2X3":
|
||||||
return False # We don't support material psets in IFC2X3 because they suck
|
return False # We don't support material psets in IFC2X3 because they suck
|
||||||
props = context.scene.BIMMaterialProperties
|
props = context.scene.BIMMaterialProperties
|
||||||
if props.materials and props.active_material_index < len(props.materials):
|
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
|
||||||
material = props.materials[props.active_material_index]
|
return True
|
||||||
if material.ifc_definition_id:
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
@@ -430,46 +429,6 @@ class BIM_PT_material_psets(Panel):
|
|||||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Material")
|
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Material")
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_material_set_psets(Panel):
|
|
||||||
bl_label = "Material Set Property Sets"
|
|
||||||
bl_idname = "BIM_PT_material_set_psets"
|
|
||||||
bl_options = {"DEFAULT_CLOSED"}
|
|
||||||
bl_space_type = "PROPERTIES"
|
|
||||||
bl_region_type = "WINDOW"
|
|
||||||
bl_context = "object"
|
|
||||||
bl_parent_id = "BIM_PT_object_material"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
if not context.active_object:
|
|
||||||
return False
|
|
||||||
if not tool.Ifc.get() or tool.Ifc.get().schema == "IFC2X3":
|
|
||||||
return False # We don't support material psets in IFC2X3 because they suck
|
|
||||||
if not tool.Ifc.get_entity(context.active_object):
|
|
||||||
return False
|
|
||||||
if not ObjectMaterialData.is_loaded:
|
|
||||||
ObjectMaterialData.load()
|
|
||||||
ifc_class = ObjectMaterialData.data["material_class"]
|
|
||||||
return bool(ifc_class and "Set" in ifc_class)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
if not MaterialSetPsetsData.is_loaded:
|
|
||||||
MaterialSetPsetsData.load()
|
|
||||||
|
|
||||||
props = context.active_object.MaterialSetPsetProperties
|
|
||||||
row = self.layout.row(align=True)
|
|
||||||
prop_with_search(row, props, "pset_name", text="")
|
|
||||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
|
||||||
op.obj = context.active_object.name
|
|
||||||
op.obj_type = "MaterialSet"
|
|
||||||
|
|
||||||
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, "MaterialSet")
|
|
||||||
|
|
||||||
for pset in MaterialSetPsetsData.data["psets"]:
|
|
||||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "MaterialSet")
|
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_material_set_item_psets(Panel):
|
class BIM_PT_material_set_item_psets(Panel):
|
||||||
bl_label = "Material Set Item Property Sets"
|
bl_label = "Material Set Item Property Sets"
|
||||||
bl_idname = "BIM_PT_material_set_item_psets"
|
bl_idname = "BIM_PT_material_set_item_psets"
|
||||||
|
|||||||
@@ -532,19 +532,19 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
"material.assign_profile", tool.Ifc.get(), material_profile=material_profile, profile=profile
|
"material.assign_profile", tool.Ifc.get(), material_profile=material_profile, profile=profile
|
||||||
)
|
)
|
||||||
elif representation_template == "WINDOW":
|
elif representation_template == "WINDOW":
|
||||||
with context.temp_override(active_object=obj):
|
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||||
bpy.ops.bim.add_window()
|
bpy.ops.bim.add_window()
|
||||||
elif representation_template == "DOOR":
|
elif representation_template == "DOOR":
|
||||||
with context.temp_override(active_object=obj):
|
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||||
bpy.ops.bim.add_door()
|
bpy.ops.bim.add_door()
|
||||||
elif representation_template == "STAIR":
|
elif representation_template == "STAIR":
|
||||||
with context.temp_override(active_object=obj):
|
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||||
bpy.ops.bim.add_stair()
|
bpy.ops.bim.add_stair()
|
||||||
elif representation_template == "RAILING":
|
elif representation_template == "RAILING":
|
||||||
with context.temp_override(active_object=obj):
|
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||||
bpy.ops.bim.add_railing()
|
bpy.ops.bim.add_railing()
|
||||||
elif representation_template == "ROOF":
|
elif representation_template == "ROOF":
|
||||||
with context.temp_override(active_object=obj):
|
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||||
bpy.ops.bim.add_roof()
|
bpy.ops.bim.add_roof()
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import bpy
|
|||||||
from . import ui, prop, operator, workspace
|
from . import ui, prop, operator, workspace
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
|
operator.ShowLoads,
|
||||||
operator.LoadStructuralAnalysisModels,
|
operator.LoadStructuralAnalysisModels,
|
||||||
operator.DisableStructuralAnalysisModelEditingUI,
|
operator.DisableStructuralAnalysisModelEditingUI,
|
||||||
operator.AddStructuralAnalysisModel,
|
operator.AddStructuralAnalysisModel,
|
||||||
@@ -83,6 +84,7 @@ classes = (
|
|||||||
ui.BIM_PT_connected_structural_members,
|
ui.BIM_PT_connected_structural_members,
|
||||||
ui.BIM_UL_structural_analysis_models,
|
ui.BIM_UL_structural_analysis_models,
|
||||||
ui.BIM_UL_structural_activities,
|
ui.BIM_UL_structural_activities,
|
||||||
|
ui.BIM_PT_show_structural_activities,
|
||||||
ui.BIM_PT_structural_load_cases,
|
ui.BIM_PT_structural_load_cases,
|
||||||
ui.BIM_UL_structural_loads,
|
ui.BIM_UL_structural_loads,
|
||||||
ui.BIM_PT_structural_loads,
|
ui.BIM_PT_structural_loads,
|
||||||
|
|||||||
@@ -30,6 +30,50 @@ def refresh():
|
|||||||
StructuralLoadCasesData.is_loaded = False
|
StructuralLoadCasesData.is_loaded = False
|
||||||
StructuralLoadsData.is_loaded = False
|
StructuralLoadsData.is_loaded = False
|
||||||
BoundaryConditionsData.is_loaded = False
|
BoundaryConditionsData.is_loaded = False
|
||||||
|
LoadGroupDecorationData.is_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
class LoadGroupDecorationData:
|
||||||
|
data = {}
|
||||||
|
is_loaded = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls):
|
||||||
|
cls.data = {"load groups to show": cls.load_groups_to_show()}
|
||||||
|
cls.is_loaded = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_groups_to_show(cls):
|
||||||
|
ret = []
|
||||||
|
abrv = {
|
||||||
|
"LOAD_CASE": "L.Case: ",
|
||||||
|
"LOAD_COMBINATION": "L.Comb: ",
|
||||||
|
"LOAD_GROUP": "L.Gr: ",
|
||||||
|
"USERDEFINED": "U.Def: ",
|
||||||
|
"NOTDEFINED": "N.Def: ",
|
||||||
|
}
|
||||||
|
models = tool.Ifc.get().by_type("IfcStructuralAnalysisModel")
|
||||||
|
m = models[0]
|
||||||
|
props = bpy.context.scene.BIMStructuralProperties
|
||||||
|
if props.activity_type == "Action":
|
||||||
|
groups = m.LoadedBy or []
|
||||||
|
for g in groups:
|
||||||
|
ret.append((str(g.id()), ". " + abrv[g.PredefinedType] + " " + g.Name, ""))
|
||||||
|
related_objects = [rel.RelatedObjects for rel in g.IsGroupedBy]
|
||||||
|
for item in related_objects:
|
||||||
|
for subgoup in [sg for sg in item if sg.is_a("IfcStructuralLoadGroup")]:
|
||||||
|
ret.append((str(subgoup.id()), ". " + abrv[subgoup.PredefinedType] + subgoup.Name, ""))
|
||||||
|
|
||||||
|
if props.activity_type == "External Reaction":
|
||||||
|
groups = m.HasResults or []
|
||||||
|
for g in groups:
|
||||||
|
result_name = g.ResultForLoadGroup.Name or ""
|
||||||
|
group_name = g.Name or ""
|
||||||
|
ret.append((str(g.id()), group_name + " " + result_name, ""))
|
||||||
|
|
||||||
|
if len(ret) == 0:
|
||||||
|
ret.append(("", "", ""))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class StructuralBoundaryConditionsData:
|
class StructuralBoundaryConditionsData:
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Bonsai - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of Bonsai.
|
||||||
|
#
|
||||||
|
# Bonsai 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.
|
||||||
|
#
|
||||||
|
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from mathutils import Vector
|
||||||
|
import numpy as np
|
||||||
|
import bpy
|
||||||
|
import gpu
|
||||||
|
import blf
|
||||||
|
from bpy.types import SpaceView3D
|
||||||
|
from gpu_extras.batch import batch_for_shader
|
||||||
|
from typing import Iterable, Union
|
||||||
|
from bonsai.bim.module.structural.load_decoration_data import ShaderInfo
|
||||||
|
|
||||||
|
|
||||||
|
class LoadsDecorator:
|
||||||
|
"""Decorator to show strucutural loads in 3D"""
|
||||||
|
|
||||||
|
is_installed = False
|
||||||
|
handlers = []
|
||||||
|
decoration_data = None
|
||||||
|
text_info = []
|
||||||
|
shader_info = []
|
||||||
|
depth_array = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def install(cls, context: bpy.types.Context) -> None:
|
||||||
|
if cls.is_installed:
|
||||||
|
cls.uninstall()
|
||||||
|
handler = cls()
|
||||||
|
cls.handlers.append(
|
||||||
|
SpaceView3D.draw_handler_add(handler.draw_load_values, ((context,)), "WINDOW", "POST_PIXEL")
|
||||||
|
)
|
||||||
|
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (), "WINDOW", "POST_VIEW"))
|
||||||
|
cls.decoration_data = ShaderInfo()
|
||||||
|
cls.update()
|
||||||
|
cls.is_installed = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def uninstall(cls) -> None:
|
||||||
|
for handler in cls.handlers:
|
||||||
|
try:
|
||||||
|
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
cls.is_installed = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update(cls) -> None:
|
||||||
|
cls.decoration_data.update()
|
||||||
|
cls.text_info = cls.decoration_data.text_info
|
||||||
|
cls.shader_info = cls.decoration_data.info
|
||||||
|
|
||||||
|
def __call__(self) -> None:
|
||||||
|
"""set gpu configurations to draw 3D representations"""
|
||||||
|
# set open gl configurations
|
||||||
|
original_blend = gpu.state.blend_get()
|
||||||
|
original_depth_test = gpu.state.depth_test_get()
|
||||||
|
gpu.state.blend_set("ALPHA")
|
||||||
|
gpu.state.depth_test_set("LESS_EQUAL")
|
||||||
|
|
||||||
|
self.draw_batch()
|
||||||
|
|
||||||
|
# restore opengl configurations
|
||||||
|
gpu.state.blend_set(original_blend)
|
||||||
|
gpu.state.depth_test_set(original_depth_test)
|
||||||
|
|
||||||
|
def draw_batch(self) -> None:
|
||||||
|
"""draw the 3D representation of loads"""
|
||||||
|
if not self.decoration_data.is_empty:
|
||||||
|
for info in self.shader_info:
|
||||||
|
shader = info["shader"]
|
||||||
|
args = info["args"]
|
||||||
|
indices = info["indices"]
|
||||||
|
batch = batch_for_shader(shader, "TRIS", args, indices=indices)
|
||||||
|
matrix = bpy.context.region_data.perspective_matrix
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("viewProjectionMatrix", matrix)
|
||||||
|
|
||||||
|
for key, value in info["uniforms"]:
|
||||||
|
shader.uniform_float(key, value)
|
||||||
|
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
def draw_load_values(self, context: bpy.types.Context) -> None:
|
||||||
|
"""draw text representing the load values"""
|
||||||
|
# getting depth buffer info, code adapted from:
|
||||||
|
# https://blender.stackexchange.com/questions/177185/is-there-a-way-to-render-depth-buffer-into-a-texture-with-gpu-bgl-python-modules
|
||||||
|
framebuffer = gpu.state.active_framebuffer_get()
|
||||||
|
width = context.region.width
|
||||||
|
height = context.region.height
|
||||||
|
depth_buffer = framebuffer.read_depth(0, 0, width, height)
|
||||||
|
depth_array = np.array(depth_buffer.to_list())
|
||||||
|
|
||||||
|
f = context.area.spaces.active.clip_end
|
||||||
|
n = context.area.spaces.active.clip_start
|
||||||
|
|
||||||
|
self.depth_array = n / (f - (f - n) * depth_array) * (f - n)
|
||||||
|
|
||||||
|
for info in self.text_info:
|
||||||
|
text_position = self.location_3d_to_region_2d(info["position"], context)
|
||||||
|
if text_position is not None:
|
||||||
|
font_id = 0
|
||||||
|
blf.position(font_id, text_position[0], text_position[1], text_position[2])
|
||||||
|
blf.size(font_id, 20.0)
|
||||||
|
blf.color(font_id, 0.9, 0.9, 0.9, 1.0)
|
||||||
|
blf.draw(font_id, info["text"])
|
||||||
|
|
||||||
|
def location_3d_to_region_2d(self, coord: Iterable, context: bpy.types.Context) -> Union[Vector, None]:
|
||||||
|
"""Convert from 3D space to 2D screen space.
|
||||||
|
Filter out the text supposed to be hidden by 3D elements, using the depth array.
|
||||||
|
It also hides text that are distant from the camera view to avoid clutter"""
|
||||||
|
|
||||||
|
coord = Vector(coord)
|
||||||
|
rv3d = context.region_data
|
||||||
|
perspective = rv3d.view_perspective
|
||||||
|
view_matrix = rv3d.view_matrix
|
||||||
|
point_view_space = view_matrix @ coord
|
||||||
|
|
||||||
|
if perspective == "ORTHO" or -10 < point_view_space.z < 0:
|
||||||
|
prj = rv3d.perspective_matrix @ Vector((coord[0], coord[1], coord[2], 1.0))
|
||||||
|
width_half = context.region.width / 2.0
|
||||||
|
height_half = context.region.height / 2.0
|
||||||
|
coord_2d = Vector(
|
||||||
|
(
|
||||||
|
width_half + width_half * (prj.x / prj.w),
|
||||||
|
height_half + height_half * (prj.y / prj.w),
|
||||||
|
point_view_space.z,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
coord_2d[0] < 0
|
||||||
|
or coord_2d[0] > context.region.width
|
||||||
|
or coord_2d[1] > context.region.height
|
||||||
|
or coord_2d[1] < 0
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
depth = self.depth_array[int(coord_2d[1])][int(coord_2d[0])]
|
||||||
|
if -0.98 * point_view_space.z > depth:
|
||||||
|
return None
|
||||||
|
return coord_2d
|
||||||
|
return None
|
||||||
@@ -0,0 +1,993 @@
|
|||||||
|
# Bonsai - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of Bonsai.
|
||||||
|
#
|
||||||
|
# Bonsai 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.
|
||||||
|
#
|
||||||
|
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import bmesh
|
||||||
|
import numpy as np
|
||||||
|
from math import sin
|
||||||
|
from mathutils import Vector
|
||||||
|
import ifcopenshell
|
||||||
|
import ifcopenshell.api
|
||||||
|
import ifcopenshell.util.attribute
|
||||||
|
import ifcopenshell.util.unit as ifcunit
|
||||||
|
import bonsai.tool as tool
|
||||||
|
from bonsai.bim.ifc import IfcStore
|
||||||
|
from bonsai.bim.module.structural.shader import DecorationShader
|
||||||
|
from typing import Literal, TypedDict, Iterable
|
||||||
|
|
||||||
|
MemberInfo = TypedDict(
|
||||||
|
"MemberInfo",
|
||||||
|
{"member": ifcopenshell.entity_instance, "activities": list[tuple[ifcopenshell.entity_instance, float]]},
|
||||||
|
)
|
||||||
|
|
||||||
|
LoadConfigItem = TypedDict(
|
||||||
|
"LoadConfigItem", {"pos": float, "descr": Literal["start", "end", "middle"], "load values": np.ndarray}
|
||||||
|
)
|
||||||
|
|
||||||
|
DiscreteConfigItem = TypedDict("DiscreteConfigItem", {"pos": float, "values": list[float]})
|
||||||
|
|
||||||
|
ParsedLoad = TypedDict(
|
||||||
|
"ParsedLoad",
|
||||||
|
{
|
||||||
|
"constant force": list[float],
|
||||||
|
"quadratic force": list[float],
|
||||||
|
"sinus force": list[float],
|
||||||
|
"linear load configuration": list[list[LoadConfigItem]],
|
||||||
|
"point load configuration": list[list[DiscreteConfigItem]],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
LoadByDirection = TypedDict(
|
||||||
|
"LoadByDirection", {"constant": float, "quadratic": float, "sinus": float, "polyline": list[list[float]]}
|
||||||
|
)
|
||||||
|
|
||||||
|
ProcessedLoad = TypedDict(
|
||||||
|
"ProcessedLoad",
|
||||||
|
{"linear loads": LoadByDirection, "max linear load": float, "discrete loads": list[list[DiscreteConfigItem]]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderInfo:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.is_empty = True
|
||||||
|
self.shader = DecorationShader()
|
||||||
|
self.curve_members: dict[str, MemberInfo] = {}
|
||||||
|
self.point_members: dict[str, MemberInfo] = {}
|
||||||
|
self.surface_members: dict[str, MemberInfo] = {}
|
||||||
|
self.text_info = []
|
||||||
|
self.info = []
|
||||||
|
self.force_unit = ""
|
||||||
|
self.moment_unit = ""
|
||||||
|
self.linear_force_unit = ""
|
||||||
|
self.linear_moment_unit = ""
|
||||||
|
self.planar_force_unit = ""
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
self.info = []
|
||||||
|
self.text_info = []
|
||||||
|
self.curve_members = {}
|
||||||
|
self.point_members = {}
|
||||||
|
self.surface_members = {}
|
||||||
|
self.get_force_units()
|
||||||
|
self.get_strucutural_elements_and_activities()
|
||||||
|
self.get_linear_loads()
|
||||||
|
self.get_point_loads()
|
||||||
|
self.get_planar_loads()
|
||||||
|
if len(self.info):
|
||||||
|
self.is_empty = False
|
||||||
|
|
||||||
|
def get_force_units(self) -> None:
|
||||||
|
def get_unit_symbol(unit: ifcopenshell.entity_instance) -> str:
|
||||||
|
prefix_symbols = {
|
||||||
|
"EXA": "E",
|
||||||
|
"PETA": "P",
|
||||||
|
"TERA": "T",
|
||||||
|
"GIGA": "G",
|
||||||
|
"MEGA": "M",
|
||||||
|
"KILO": "k",
|
||||||
|
"HECTO": "h",
|
||||||
|
"DECA": "da",
|
||||||
|
"DECI": "d",
|
||||||
|
"CENTI": "c",
|
||||||
|
"MILLI": "m",
|
||||||
|
"MICRO": "μ",
|
||||||
|
"NANO": "n",
|
||||||
|
"PICO": "p",
|
||||||
|
"FEMTO": "f",
|
||||||
|
"ATTO": "a",
|
||||||
|
}
|
||||||
|
|
||||||
|
unit_symbols = {
|
||||||
|
# si units
|
||||||
|
"CUBIC_METRE": "m3",
|
||||||
|
"GRAM": "g",
|
||||||
|
"SECOND": "s",
|
||||||
|
"SQUARE_METRE": "m2",
|
||||||
|
"METRE": "m",
|
||||||
|
"NEWTON": "N",
|
||||||
|
"PASCAL": "Pa",
|
||||||
|
# conversion based units
|
||||||
|
"pound-force": "lbf",
|
||||||
|
"pound-force per square inch": "psi",
|
||||||
|
"thou": "th",
|
||||||
|
"inch": "in",
|
||||||
|
"foot": "ft",
|
||||||
|
"yard": "yd",
|
||||||
|
"mile": "mi",
|
||||||
|
"square thou": "th2",
|
||||||
|
"square inch": "in2",
|
||||||
|
"square foot": "ft2",
|
||||||
|
"square yard": "yd2",
|
||||||
|
"acre": "ac",
|
||||||
|
"square mile": "mi2",
|
||||||
|
"cubic thou": "th3",
|
||||||
|
"cubic inch": "in3",
|
||||||
|
"cubic foot": "ft3",
|
||||||
|
"cubic yard": "yd3",
|
||||||
|
"cubic mile": "mi3",
|
||||||
|
"litre": "L",
|
||||||
|
"fluid ounce UK": "fl oz",
|
||||||
|
"fluid ounce US": "fl oz",
|
||||||
|
"pint UK": "pt",
|
||||||
|
"pint US": "pt",
|
||||||
|
"gallon UK": "gal",
|
||||||
|
"gallon US": "gal",
|
||||||
|
"degree": "°",
|
||||||
|
"ounce": "oz",
|
||||||
|
"pound": "lb",
|
||||||
|
"ton UK": "ton",
|
||||||
|
"ton US": "ton",
|
||||||
|
"lbf": "lbf",
|
||||||
|
"kip": "kip",
|
||||||
|
"psi": "psi",
|
||||||
|
"ksi": "ksi",
|
||||||
|
"minute": "min",
|
||||||
|
"hour": "hr",
|
||||||
|
"day": "day",
|
||||||
|
"btu": "btu",
|
||||||
|
"fahrenheit": "°F",
|
||||||
|
}
|
||||||
|
symbol = ""
|
||||||
|
if unit.is_a("IfcSIUnit"):
|
||||||
|
symbol += prefix_symbols.get(unit.Prefix, "")
|
||||||
|
symbol += unit_symbols.get(unit.Name.replace("METER", "METRE"), "?")
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
length_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "LENGTHUNIT"]
|
||||||
|
force_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "FORCEUNIT"]
|
||||||
|
linear_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARFORCEUNIT"]
|
||||||
|
linear_moment_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARMOMENTUNIT"]
|
||||||
|
planar_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "PLANARFORCEUNIT"]
|
||||||
|
|
||||||
|
conversion_force_unit = [u for u in force_units if u.is_a("IfcConversionBasedUnit")]
|
||||||
|
if len(conversion_force_unit) == 0:
|
||||||
|
conversion_force_unit.append(force_units[0])
|
||||||
|
self.force_unit = ifcunit.get_unit_symbol(conversion_force_unit[0])
|
||||||
|
|
||||||
|
conversion_length_unit = [u for u in length_units if u.is_a("IfcConversionBasedUnit")]
|
||||||
|
if len(conversion_length_unit) == 0:
|
||||||
|
conversion_length_unit.append(length_units[0])
|
||||||
|
length_unit = ifcunit.get_unit_symbol(conversion_length_unit[0])
|
||||||
|
self.moment_unit = self.force_unit + "." + length_unit
|
||||||
|
|
||||||
|
first = ""
|
||||||
|
second = ""
|
||||||
|
for e in linear_force_units[0].Elements:
|
||||||
|
if e.Unit.UnitType == "FORCEUNIT":
|
||||||
|
first = ifcunit.get_unit_symbol(e.Unit)
|
||||||
|
if e.Unit.UnitType == "LENGTHUNIT":
|
||||||
|
second = ifcunit.get_unit_symbol(e.Unit)
|
||||||
|
self.linear_force_unit = first + "/" + second
|
||||||
|
|
||||||
|
first = ""
|
||||||
|
second = ""
|
||||||
|
for e in linear_moment_units[0].Elements:
|
||||||
|
if e.Unit.UnitType == "FORCEUNIT":
|
||||||
|
first = ifcunit.get_unit_symbol(e.Unit)
|
||||||
|
if e.Unit.UnitType == "LENGTHUNIT":
|
||||||
|
second = ifcunit.get_unit_symbol(e.Unit)
|
||||||
|
self.linear_moment_unit = first + "." + second + "/" + second
|
||||||
|
|
||||||
|
first = ""
|
||||||
|
second = ""
|
||||||
|
for e in planar_force_units[0].Elements:
|
||||||
|
if e.Unit.UnitType == "FORCEUNIT":
|
||||||
|
first = ifcunit.get_unit_symbol(e.Unit)
|
||||||
|
if e.Unit.UnitType == "LENGTHUNIT":
|
||||||
|
second = ifcunit.get_unit_symbol(e.Unit) + "2"
|
||||||
|
if e.Unit.UnitType == "AREAUNIT":
|
||||||
|
second = ifcunit.get_unit_symbol(e.Unit)
|
||||||
|
self.planar_force_unit = first + "/" + second
|
||||||
|
|
||||||
|
def get_strucutural_elements_and_activities(self) -> None:
|
||||||
|
"""fills self.point_members, self.curve_members and self.surface_members dictionaries"""
|
||||||
|
|
||||||
|
def populate_members_dict(
|
||||||
|
dict_name: Literal["point_members", "curve_members", "surface_members"],
|
||||||
|
element: ifcopenshell.entity_instance,
|
||||||
|
activity: ifcopenshell.entity_instance,
|
||||||
|
factor: float,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
fills self.point_members, self.curve_members and self.surface_members dictionaries
|
||||||
|
thoses dicts will contain the strucutural member global id as key and a second dict as value
|
||||||
|
the second dict contais two keys, as follow:
|
||||||
|
{
|
||||||
|
"member": the strucutral member itself
|
||||||
|
"activities: list[(activity, factor)] for each activity applied to the member
|
||||||
|
}
|
||||||
|
dict_name: "point_members", "curve_members" or "surface_members"
|
||||||
|
element: IfcStructuralMember
|
||||||
|
activity: IfcStructuralActivity
|
||||||
|
factor: float to multiply the loads values in the structural activity
|
||||||
|
"""
|
||||||
|
dic = getattr(self, dict_name, None)
|
||||||
|
if dic is None:
|
||||||
|
return
|
||||||
|
member = dic.get(element.GlobalId)
|
||||||
|
if member is None:
|
||||||
|
dic.update({element.GlobalId: {"member": element, "activities": [(activity, factor)]}})
|
||||||
|
else:
|
||||||
|
member["activities"].append((activity, factor))
|
||||||
|
|
||||||
|
def recursive_subgroups(
|
||||||
|
groups: list[ifcopenshell.entity_instance],
|
||||||
|
rec_limit: int,
|
||||||
|
activity_type: Literal["Action", "External Reaction"],
|
||||||
|
factor: float = 1,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Recursively fills self.point_members, self.curve_members and self.surface_members dictionaries
|
||||||
|
with the structural members to wich the activities in the load group and its subgroups are applied
|
||||||
|
it also creates a list with all the activities applied to that member that are in the same
|
||||||
|
load group or subgroup (see populate_members_dict description)
|
||||||
|
|
||||||
|
groups: list of Ifc load case, load group or load combination
|
||||||
|
rec_limit: maximum number of recursions
|
||||||
|
activity_type: "Action" or "External Reaction"
|
||||||
|
factor: the factor applied to the group, default = 1
|
||||||
|
this factor will be multiplied by the group coefficient and the factor of a
|
||||||
|
IfcRelAssignsToGroupByFactor relationship of subgroups in load combinations
|
||||||
|
|
||||||
|
"""
|
||||||
|
if rec_limit == 0 or len(groups) == 0:
|
||||||
|
return None
|
||||||
|
for group in groups:
|
||||||
|
subgorups = []
|
||||||
|
activities = []
|
||||||
|
relationship = [rel for rel in group.IsGroupedBy]
|
||||||
|
coef = getattr(group, "Coefficient", 1.0)
|
||||||
|
group_coef = coef if coef is not None else 1.0
|
||||||
|
rel_factor = 1.0
|
||||||
|
|
||||||
|
for rel in relationship:
|
||||||
|
if rel.is_a("IfcRelAssignsToGroupByFactor"):
|
||||||
|
rel_factor = rel.Factor if rel.Factor is not None else 1.0
|
||||||
|
objects = rel.RelatedObjects
|
||||||
|
subgorups = [sg for sg in objects if sg.is_a("IfcStructuralLoadGroup")]
|
||||||
|
activities = [a for a in objects if a.is_a("IfcStructuralActivity")]
|
||||||
|
factor = factor * group_coef * rel_factor
|
||||||
|
for activity in activities:
|
||||||
|
if len(activity.AssignedToStructuralItem):
|
||||||
|
element = activity.AssignedToStructuralItem[0].RelatingElement
|
||||||
|
if element is not None:
|
||||||
|
if activity_type == "Action":
|
||||||
|
if element.is_a("IfcStructuralCurveMember"):
|
||||||
|
populate_members_dict("curve_members", element, activity, factor)
|
||||||
|
elif element.is_a("IfcStructuralPointConnection"):
|
||||||
|
populate_members_dict("point_members", element, activity, factor)
|
||||||
|
elif element.is_a("IfcStructuralSurfaceMember"):
|
||||||
|
populate_members_dict("surface_members", element, activity, factor)
|
||||||
|
|
||||||
|
elif (
|
||||||
|
activity_type == "External Reaction"
|
||||||
|
and getattr(element, "AppliedCondition", None) is not None
|
||||||
|
):
|
||||||
|
if element.is_a("IfcStructuralCurveMember"):
|
||||||
|
populate_members_dict("curve_members", element, activity, factor)
|
||||||
|
elif element.is_a("IfcStructuralPointConnection"):
|
||||||
|
populate_members_dict("point_members", element, activity, factor)
|
||||||
|
elif element.is_a("IfcStructuralSurfaceMember"):
|
||||||
|
populate_members_dict("surface_members", element, activity, factor)
|
||||||
|
recursive_subgroups(subgorups, rec_limit - 1, activity_type, factor=factor)
|
||||||
|
|
||||||
|
props = bpy.context.scene.BIMStructuralProperties
|
||||||
|
group_definition_id = int(props.load_group_to_show)
|
||||||
|
file = IfcStore.get_file()
|
||||||
|
groups = [file.by_id(group_definition_id)]
|
||||||
|
recursive_subgroups(groups, 10, props.activity_type)
|
||||||
|
|
||||||
|
def get_planar_loads(self) -> None:
|
||||||
|
"""get the necessary information to render the planar load representation in 3D and its text information"""
|
||||||
|
|
||||||
|
list_of_surfaces = self.surface_members
|
||||||
|
shader = self.shader.new("PLANAR LOAD")
|
||||||
|
maximum = 0
|
||||||
|
for value in list_of_surfaces.values():
|
||||||
|
surf = value["member"]
|
||||||
|
activity_list = value["activities"]
|
||||||
|
if len(activity_list) == 0:
|
||||||
|
continue
|
||||||
|
rotation = self.get_surface_member_rotation(surf)
|
||||||
|
values = self.get_planar_loads_values(activity_list, rotation)
|
||||||
|
if maximum == 0:
|
||||||
|
maximum = max([abs(float(i)) for i in values])
|
||||||
|
if maximum == 0:
|
||||||
|
continue
|
||||||
|
props = bpy.context.scene.BIMStructuralProperties
|
||||||
|
reference_frame = props.reference_frame
|
||||||
|
orientation = np.eye(3)
|
||||||
|
if reference_frame == "LOCAL_COORDS":
|
||||||
|
orientation = rotation
|
||||||
|
blender_object: bpy.types.Object = IfcStore.get_element(getattr(surf, "GlobalId", None))
|
||||||
|
mat = blender_object.matrix_world
|
||||||
|
mesh: bpy.types.Mesh = blender_object.data
|
||||||
|
|
||||||
|
positions = []
|
||||||
|
indices = []
|
||||||
|
coord = []
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(mesh)
|
||||||
|
bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||||
|
bm.edges.ensure_lookup_table()
|
||||||
|
bm.verts.ensure_lookup_table()
|
||||||
|
bm.faces.ensure_lookup_table()
|
||||||
|
|
||||||
|
for v in bm.verts:
|
||||||
|
p1 = np.array(mat @ v.co)
|
||||||
|
positions.append(p1)
|
||||||
|
p2 = p1 - (orientation @ values) * 0.2 / maximum
|
||||||
|
positions.append(p2)
|
||||||
|
coord.append((float(p1[0] + p1[1]), 0, 1))
|
||||||
|
coord.append((float(p1[0] + p1[1]), 1, 1))
|
||||||
|
for e in bm.edges:
|
||||||
|
if len(e.link_faces) > 1:
|
||||||
|
continue
|
||||||
|
indices.append((2 * e.verts[0].index, 2 * e.verts[0].index + 1, 2 * e.verts[1].index))
|
||||||
|
indices.append((2 * e.verts[0].index + 1, 2 * e.verts[1].index, 2 * e.verts[1].index + 1))
|
||||||
|
for p in bm.faces:
|
||||||
|
indices.append((2 * p.verts[0].index + 1, 2 * p.verts[1].index + 1, 2 * p.verts[2].index + 1))
|
||||||
|
bmesh.ops.dissolve_limit(bm, angle_limit=0.01, verts=bm.verts, edges=bm.edges)
|
||||||
|
bm.faces.ensure_lookup_table()
|
||||||
|
center = bm.faces[0].calc_center_bounds()
|
||||||
|
|
||||||
|
self.text_info.append(
|
||||||
|
{
|
||||||
|
"position": mat @ center - Vector((orientation @ values) * 0.2 / maximum),
|
||||||
|
"text": f"{values[2]:.5f} {self.planar_force_unit}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.info.append(
|
||||||
|
{
|
||||||
|
"shader": shader,
|
||||||
|
"args": {"position": positions, "coord": coord},
|
||||||
|
"indices": indices,
|
||||||
|
"uniforms": [["color", (0.2, 0, 1, 1)], ["spacing", 0.2]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_planar_loads_values(
|
||||||
|
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
returns a numpy array with the sum of the values of structural activities
|
||||||
|
applied loads in each direction, multiplied by the factors in load combinations
|
||||||
|
"""
|
||||||
|
values = np.zeros((3))
|
||||||
|
for item in activity_list:
|
||||||
|
activity = item[0]
|
||||||
|
factor = item[1]
|
||||||
|
load = activity.AppliedLoad
|
||||||
|
temp = np.zeros((3))
|
||||||
|
if load is not None and load.is_a("IfcStructuralLoadPlanarForce"):
|
||||||
|
temp[0] = load.PlanarForceX if load.PlanarForceX is not None else 0
|
||||||
|
temp[1] = load.PlanarForceY if load.PlanarForceY is not None else 0
|
||||||
|
temp[2] = load.PlanarForceZ if load.PlanarForceZ is not None else 0
|
||||||
|
temp = temp * factor
|
||||||
|
transform = self.get_activity_transform_matrix(activity, element_rotation_matrix)
|
||||||
|
values += transform @ temp
|
||||||
|
return values
|
||||||
|
|
||||||
|
def get_surface_member_rotation(self, surface_member: ifcopenshell.entity_instance) -> np.ndarray:
|
||||||
|
"""returns the rotation matrix of a structural surface member"""
|
||||||
|
representation = ifcopenshell.util.representation.get_representation(surface_member, "Model")
|
||||||
|
repr_item = representation.Items[0]
|
||||||
|
placement = ifcopenshell.util.placement.get_axis2placement(repr_item.FaceSurface.Position)
|
||||||
|
rotation = placement[0:3, 0:3]
|
||||||
|
return rotation
|
||||||
|
|
||||||
|
def get_point_connection_rotation(self, point_connection: ifcopenshell.entity_instance) -> np.ndarray:
|
||||||
|
"""returns the rotation matrix of a structural point connection"""
|
||||||
|
if point_connection.ConditionCoordinateSystem is not None:
|
||||||
|
placement = ifcopenshell.util.placement.get_axis2placement(point_connection.ConditionCoordinateSystem)
|
||||||
|
else:
|
||||||
|
placement = np.eye(4)
|
||||||
|
rotation = placement[0:3, 0:3]
|
||||||
|
return rotation
|
||||||
|
|
||||||
|
def get_curve_member_rotation(self, curve_member: ifcopenshell.entity_instance) -> np.ndarray:
|
||||||
|
"""returns the rotation matrix of a structural surface member"""
|
||||||
|
z = curve_member.Axis.DirectionRatios
|
||||||
|
edge = curve_member.Representation.Representations[0].Items[0]
|
||||||
|
origin = edge.EdgeStart.VertexGeometry.Coordinates
|
||||||
|
end = edge.EdgeEnd.VertexGeometry.Coordinates
|
||||||
|
x = [c2 - c1 for c1, c2 in zip(origin, end)]
|
||||||
|
placement = ifcopenshell.util.placement.a2p(origin, z, x)
|
||||||
|
rotation = placement[0:3, 0:3]
|
||||||
|
return rotation
|
||||||
|
|
||||||
|
def get_activity_transform_matrix(
|
||||||
|
self, activity: ifcopenshell.entity_instance, element_rotation_matrix: np.ndarray
|
||||||
|
) -> np.ndarray:
|
||||||
|
"provides the transformation matrix to convert between reference frames"
|
||||||
|
global_or_local = activity.GlobalOrLocal
|
||||||
|
props = bpy.context.scene.BIMStructuralProperties
|
||||||
|
reference_frame = props.reference_frame
|
||||||
|
transform_matrix = np.eye(3)
|
||||||
|
if reference_frame == "LOCAL_COORDS" and global_or_local != reference_frame:
|
||||||
|
transform_matrix = np.linalg.inv(element_rotation_matrix)
|
||||||
|
elif reference_frame == "GLOBAL_COORDS" and global_or_local != reference_frame:
|
||||||
|
transform_matrix = element_rotation_matrix
|
||||||
|
return transform_matrix
|
||||||
|
|
||||||
|
def get_point_loads(self) -> None:
|
||||||
|
"""get the necessary information to render the point load representation in 3D and its text information"""
|
||||||
|
|
||||||
|
list_of_point_connections = self.point_members
|
||||||
|
for value in list_of_point_connections.values():
|
||||||
|
conn = value["member"]
|
||||||
|
activity_list = value["activities"]
|
||||||
|
if len(activity_list) == 0:
|
||||||
|
continue
|
||||||
|
blender_object = IfcStore.get_element(getattr(conn, "GlobalId", None))
|
||||||
|
if blender_object.type == "MESH":
|
||||||
|
conn_location = blender_object.matrix_world @ blender_object.data.vertices[0].co
|
||||||
|
rotation = self.get_point_connection_rotation(conn)
|
||||||
|
loads = self.get_point_loads_values(activity_list, rotation)
|
||||||
|
self.get_point_shader_args(loads, conn_location, rotation)
|
||||||
|
|
||||||
|
def get_point_shader_args(self, loads: Iterable, location: np.ndarray, rotation: np.ndarray) -> None:
|
||||||
|
"""get the args to the point shader"""
|
||||||
|
location = np.array(location)
|
||||||
|
indices = []
|
||||||
|
direction_dict = {
|
||||||
|
"fx": (np.array((1, 0, 0)), np.array((0, 1, 0)), np.array((0, 0, 1))),
|
||||||
|
"fy": (np.array((0, 1, 0)), np.array((1, 0, 0)), np.array((0, 0, 1))),
|
||||||
|
"fz": (np.array((0, 0, 1)), np.array((0, 1, 0)), np.array((1, 0, 0))),
|
||||||
|
"mx": (np.array((0, 1, 0)), np.array((0, 0, 1))),
|
||||||
|
"my": (np.array((1, 0, 0)), np.array((0, 0, 1))),
|
||||||
|
"mz": (np.array((1, 0, 0)), np.array((0, 1, 0))),
|
||||||
|
}
|
||||||
|
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
|
||||||
|
props = bpy.context.scene.BIMStructuralProperties
|
||||||
|
reference_frame = props.reference_frame
|
||||||
|
if reference_frame == "LOCAL_COORDS":
|
||||||
|
for key in keys:
|
||||||
|
tup = direction_dict[key]
|
||||||
|
li = []
|
||||||
|
for item in tup:
|
||||||
|
li.append(rotation @ item)
|
||||||
|
direction_dict[key] = li
|
||||||
|
|
||||||
|
for i, key in enumerate(keys):
|
||||||
|
if loads[i] == 0:
|
||||||
|
continue
|
||||||
|
color = (1, 0, 0, 1)
|
||||||
|
if i in [1, 4]:
|
||||||
|
color = (0, 1, 0, 1)
|
||||||
|
elif i in [2, 5]:
|
||||||
|
color = (0, 0, 1, 1)
|
||||||
|
d1 = -(direction_dict[key][0] * loads[i])
|
||||||
|
d1 = d1 / np.linalg.norm(d1)
|
||||||
|
if i < 3:
|
||||||
|
d2 = direction_dict[key][1]
|
||||||
|
d3 = direction_dict[key][2]
|
||||||
|
p1 = location
|
||||||
|
p2 = location + d1 + d2
|
||||||
|
p3 = location + d1 - d2
|
||||||
|
p4 = location + d1 + d3
|
||||||
|
p5 = location + d1 - d3
|
||||||
|
position = [p1, p2, p3, p4, p5]
|
||||||
|
indices = [(0, 1, 2), (0, 3, 4)]
|
||||||
|
c1 = (0, 0, 0)
|
||||||
|
c2 = (1, 1, 0)
|
||||||
|
c3 = (-1, 1, 0)
|
||||||
|
coords_for_shader = [c1, c2, c3, c2, c3]
|
||||||
|
shader = self.shader.new("SINGLE FORCE")
|
||||||
|
self.info.append(
|
||||||
|
{
|
||||||
|
"shader": shader,
|
||||||
|
"args": {"position": position, "coord": coords_for_shader},
|
||||||
|
"indices": indices,
|
||||||
|
"uniforms": [["color", color], ["spacing", 0.2]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.text_info.append({"position": location + d1, "text": f"{loads[i]:.2f} {self.force_unit}"})
|
||||||
|
else:
|
||||||
|
d2 = d2 = direction_dict[key][1]
|
||||||
|
p1 = location - d2
|
||||||
|
p2 = location + d1 + d2
|
||||||
|
p3 = location - d1 + d2
|
||||||
|
position = [p1, p2, p3]
|
||||||
|
indices = [(0, 1, 2)]
|
||||||
|
c1 = (-1, 0, 0)
|
||||||
|
c2 = (1, 1, 0)
|
||||||
|
c3 = (1, -1, 0)
|
||||||
|
coords_for_shader = [c1, c2, c3]
|
||||||
|
shader = self.shader.new("SINGLE MOMENT")
|
||||||
|
self.info.append(
|
||||||
|
{
|
||||||
|
"shader": shader,
|
||||||
|
"args": {"position": position, "coord": coords_for_shader},
|
||||||
|
"indices": indices,
|
||||||
|
"uniforms": [["color", color]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.text_info.append(
|
||||||
|
{"position": location + 0.25 * (d1 + d2), "text": f"{loads[i]:.2f} {self.moment_unit}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_point_loads_values(
|
||||||
|
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""returns a numpy array with the sum of the point load values"""
|
||||||
|
result_list = np.zeros(6)
|
||||||
|
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
|
||||||
|
for item in activity_list:
|
||||||
|
activity = item[0]
|
||||||
|
factor = item[1]
|
||||||
|
load = activity.AppliedLoad
|
||||||
|
temp = np.zeros(6)
|
||||||
|
for i, attr in enumerate(attr_list):
|
||||||
|
value = 0 if getattr(load, attr, 0) is None else getattr(load, attr, 0)
|
||||||
|
temp[i] += value * factor
|
||||||
|
transform_3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
|
||||||
|
transform_6 = np.zeros((6, 6))
|
||||||
|
transform_6[0:3, 0:3] = transform_3
|
||||||
|
transform_6[3:6, 3:6] = transform_3
|
||||||
|
result_list += transform_6 @ temp
|
||||||
|
|
||||||
|
return result_list
|
||||||
|
|
||||||
|
def get_linear_loads(self) -> None:
|
||||||
|
position = []
|
||||||
|
indices = []
|
||||||
|
sin_quad_lin = []
|
||||||
|
coords_for_shader = []
|
||||||
|
color = []
|
||||||
|
info = []
|
||||||
|
maxforce = 0
|
||||||
|
|
||||||
|
list_of_curve_members = self.curve_members
|
||||||
|
for value in list_of_curve_members.values():
|
||||||
|
member = value["member"]
|
||||||
|
activity_list = value["activities"]
|
||||||
|
if len(activity_list) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
blender_object = IfcStore.get_element(getattr(member, "GlobalId", None))
|
||||||
|
|
||||||
|
start_co = blender_object.matrix_world @ blender_object.data.vertices[0].co
|
||||||
|
end_co = blender_object.matrix_world @ blender_object.data.vertices[1].co
|
||||||
|
x_axis = Vector(end_co - start_co).normalized()
|
||||||
|
z_direction = getattr(member, "Axis")
|
||||||
|
# local coordinates
|
||||||
|
z_axis = Vector(getattr(z_direction, "DirectionRatios", None)).normalized()
|
||||||
|
y_axis = z_axis.cross(x_axis).normalized()
|
||||||
|
z_axis = x_axis.cross(y_axis).normalized()
|
||||||
|
rotation = self.get_curve_member_rotation(member)
|
||||||
|
|
||||||
|
props = bpy.context.scene.BIMStructuralProperties
|
||||||
|
reference_frame = props.reference_frame
|
||||||
|
is_local = reference_frame == "LOCAL_COORDS"
|
||||||
|
x_match = abs(Vector((1, 0, 0)).dot(x_axis)) > 0.99
|
||||||
|
y_match = abs(Vector((0, 1, 0)).dot(x_axis)) > 0.99
|
||||||
|
z_match = abs(Vector((0, 0, 1)).dot(x_axis)) > 0.99
|
||||||
|
direction_dict = {
|
||||||
|
"fx": y_axis + z_axis if is_local else Vector((1, 0, 0)) if not x_match else Vector((0, 1, 1)),
|
||||||
|
"fy": y_axis if is_local else Vector((0, 1, 0)) if not y_match else Vector((1, 0, 1)),
|
||||||
|
"fz": z_axis if is_local else Vector((0, 0, 1)) if not z_match else Vector((1, 1, 0)),
|
||||||
|
"mx": z_axis - y_axis if is_local or x_match else Vector((1, 0, 0)).cross(x_axis),
|
||||||
|
"my": (
|
||||||
|
z_axis
|
||||||
|
if is_local
|
||||||
|
else Vector((-1, 0, 1)) if y_match else Vector((0, 1, 0)).cross(x_axis).normalized()
|
||||||
|
),
|
||||||
|
"mz": (
|
||||||
|
y_axis
|
||||||
|
if is_local
|
||||||
|
else Vector((-1, 1, 0)) if z_match else Vector((0, 0, 1)).cross(x_axis).normalized()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
match_dict = {"fx": x_match or is_local, "fy": y_match, "fz": z_match}
|
||||||
|
member_length = Vector(end_co - start_co).length
|
||||||
|
processed_loads = self.process_total_linear_loads(activity_list, rotation, member_length)
|
||||||
|
linear_loads = processed_loads["linear loads"]
|
||||||
|
maxforce = max(maxforce, processed_loads["max linear load"])
|
||||||
|
point_loads = processed_loads["discrete loads"]
|
||||||
|
|
||||||
|
if len(point_loads) > 0:
|
||||||
|
for item in point_loads:
|
||||||
|
for sub_item in item:
|
||||||
|
pos = sub_item["pos"]
|
||||||
|
values = sub_item["values"]
|
||||||
|
pos_vector = start_co + x_axis * pos
|
||||||
|
self.get_point_shader_args(values, pos_vector, rotation)
|
||||||
|
if linear_loads is None:
|
||||||
|
continue
|
||||||
|
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
polyline = linear_loads[key]["polyline"]
|
||||||
|
sinus = linear_loads[key]["sinus"]
|
||||||
|
quadratic = linear_loads[key]["quadratic"]
|
||||||
|
constant = linear_loads[key]["constant"]
|
||||||
|
direction = direction_dict[key]
|
||||||
|
color_axis = (0, 0, 1, 1)
|
||||||
|
if "x" in key:
|
||||||
|
color_axis = (1, 0, 0, 1)
|
||||||
|
if "y" in key:
|
||||||
|
color_axis = (0, 1, 0, 1)
|
||||||
|
|
||||||
|
if "f" in key:
|
||||||
|
unit = self.linear_force_unit
|
||||||
|
if match_dict[key]:
|
||||||
|
shader = self.shader.new("PARALLEL DISTRIBUTED FORCE")
|
||||||
|
else:
|
||||||
|
shader = self.shader.new("PERPENDICULAR DISTRIBUTED FORCE")
|
||||||
|
else:
|
||||||
|
unit = self.linear_moment_unit
|
||||||
|
shader = self.shader.new("DISTRIBUTED MOMENT")
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
for i in range(len(polyline) - 1):
|
||||||
|
current = Vector(polyline[i] + [0])
|
||||||
|
nextitem = Vector(polyline[i + 1] + [0])
|
||||||
|
|
||||||
|
if any([current.y, nextitem.y, constant, quadratic, sinus]):
|
||||||
|
negative = -1 * direction + start_co + x_axis * current.x
|
||||||
|
positive = direction + start_co + x_axis * current.x
|
||||||
|
position.append(negative)
|
||||||
|
coords_for_shader.append((current.x, 1.0, member_length))
|
||||||
|
sin_quad_lin.append((sinus, quadratic, current.y + constant))
|
||||||
|
color.append(color_axis)
|
||||||
|
|
||||||
|
x = current.x / member_length
|
||||||
|
func = sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + current.y
|
||||||
|
if func:
|
||||||
|
self.text_info.append(
|
||||||
|
{
|
||||||
|
"position": -1 * direction * func / maxforce + start_co + x_axis * current.x,
|
||||||
|
"text": f"{func:.2f} {unit}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
position.append(positive)
|
||||||
|
coords_for_shader.append((current[0], -1.0, member_length))
|
||||||
|
sin_quad_lin.append((sinus, quadratic, current.y + constant))
|
||||||
|
color.append(color_axis)
|
||||||
|
|
||||||
|
indices.append((0 + counter, 1 + counter, 2 + counter))
|
||||||
|
indices.append((3 + counter, 2 + counter, 1 + counter))
|
||||||
|
if i == len(polyline) - 2:
|
||||||
|
negative = -1 * direction + start_co + x_axis * nextitem.x
|
||||||
|
positive = direction + start_co + x_axis * nextitem.x
|
||||||
|
position.append(negative)
|
||||||
|
coords_for_shader.append((nextitem.x, 1.0, member_length))
|
||||||
|
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
|
||||||
|
color.append(color_axis)
|
||||||
|
|
||||||
|
x = nextitem.x / member_length
|
||||||
|
func = (
|
||||||
|
sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + nextitem.y
|
||||||
|
)
|
||||||
|
if func:
|
||||||
|
self.text_info.append(
|
||||||
|
{
|
||||||
|
"position": -1 * direction * func / maxforce + start_co + x_axis * nextitem.x,
|
||||||
|
"text": f"{func:.2f} {unit}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
position.append(positive)
|
||||||
|
coords_for_shader.append((nextitem.x, -1.0, member_length))
|
||||||
|
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
|
||||||
|
color.append(color_axis)
|
||||||
|
|
||||||
|
counter += 2
|
||||||
|
if position:
|
||||||
|
self.info.append(
|
||||||
|
{
|
||||||
|
"shader": shader,
|
||||||
|
"args": {
|
||||||
|
"position": position,
|
||||||
|
"sin_quad_lin_forces": sin_quad_lin,
|
||||||
|
"coord": coords_for_shader,
|
||||||
|
},
|
||||||
|
"indices": indices,
|
||||||
|
"uniforms": [["color", color_axis], ["spacing", 0.2], ["maxload", maxforce]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
position = []
|
||||||
|
sin_quad_lin = []
|
||||||
|
coords_for_shader = []
|
||||||
|
indices = []
|
||||||
|
for info in self.info:
|
||||||
|
info["uniforms"][2][1] = maxforce
|
||||||
|
|
||||||
|
def process_total_linear_loads(
|
||||||
|
self,
|
||||||
|
activity_list: list[tuple[ifcopenshell.entity_instance, float]],
|
||||||
|
element_rotation_matrix: np.ndarray,
|
||||||
|
member_length: float,
|
||||||
|
) -> ProcessedLoad:
|
||||||
|
"""returns a dict with total values for applied loads in each direction
|
||||||
|
along with the maximum value for the loads in the member and the discrete loads applied
|
||||||
|
|
||||||
|
"""
|
||||||
|
loads_dict = self.parse_linear_loads_to_dict(activity_list, element_rotation_matrix)
|
||||||
|
const = loads_dict["constant force"]
|
||||||
|
quad = loads_dict["quadratic force"]
|
||||||
|
sinus = loads_dict["sinus force"]
|
||||||
|
loads = loads_dict["linear load configuration"]
|
||||||
|
unique_list = self.getuniquepositionlist(loads)
|
||||||
|
final_list = []
|
||||||
|
distributed_loads = None
|
||||||
|
max_load = 0
|
||||||
|
for pos in unique_list:
|
||||||
|
value = self.get_before_and_after(pos, loads)
|
||||||
|
if value["before"] == value["after"]:
|
||||||
|
final_list.append([pos] + value["before"])
|
||||||
|
else:
|
||||||
|
final_list.append([pos] + value["before"])
|
||||||
|
final_list.append([pos] + value["after"])
|
||||||
|
|
||||||
|
if len(final_list) == 0 and any(const + quad + sinus):
|
||||||
|
final_list.append([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||||
|
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||||
|
|
||||||
|
elif len(final_list) > 0:
|
||||||
|
if final_list[0][0] and any(
|
||||||
|
const + quad + sinus
|
||||||
|
): # if first item location is not 0 append an item at the zero
|
||||||
|
final_list = [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] + final_list
|
||||||
|
else:
|
||||||
|
del final_list[0]
|
||||||
|
if abs(final_list[-1][0] - member_length) > 0.01 and any(const + quad + sinus):
|
||||||
|
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||||
|
else:
|
||||||
|
del final_list[-1]
|
||||||
|
if len(final_list) > 0:
|
||||||
|
array = np.array(final_list) # 7xn -> ["pos","fx","fy","fz","mx","my","mz"]
|
||||||
|
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
|
||||||
|
polyline = []
|
||||||
|
distributed_loads = {
|
||||||
|
"fx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||||
|
"fy": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||||
|
"fz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||||
|
"mx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||||
|
"my": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||||
|
"mz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||||
|
}
|
||||||
|
max_load = 0
|
||||||
|
for component, key in enumerate(keys):
|
||||||
|
if any([sinus[component], quad[component], const[component]]) or any(
|
||||||
|
item for item in array[:, component + 1]
|
||||||
|
):
|
||||||
|
|
||||||
|
for currentitem in final_list:
|
||||||
|
polyline.append([currentitem[0], currentitem[component + 1]])
|
||||||
|
max_load = max(
|
||||||
|
max_load,
|
||||||
|
abs(sinus[component] + quad[component] + const[component] + currentitem[component + 1]),
|
||||||
|
)
|
||||||
|
inner_dict = distributed_loads[key]
|
||||||
|
inner_dict["constant"] = const[component]
|
||||||
|
inner_dict["quadratic"] = quad[component]
|
||||||
|
inner_dict["sinus"] = sinus[component]
|
||||||
|
inner_dict["polyline"] = polyline
|
||||||
|
distributed_loads[key] = inner_dict
|
||||||
|
|
||||||
|
return {
|
||||||
|
"linear loads": distributed_loads,
|
||||||
|
"max linear load": max_load,
|
||||||
|
"discrete loads": loads_dict["point load configuration"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def getuniquepositionlist(self, load_config_list: list[list[LoadConfigItem]]) -> list[float]:
|
||||||
|
"""return an ordereded list of unique locations based on the load configuration list
|
||||||
|
ex: load_config_list = [[{"pos":1.0,...},{"pos":3.0,...}],
|
||||||
|
[{"pos":2.0,...},{"pos":3.0,...}],
|
||||||
|
[{"pos":1.5,...},{"pos":2.5,...}]]
|
||||||
|
return = [1.0, 1.5, 2.0, 2.5, 3.0]
|
||||||
|
"""
|
||||||
|
unique = []
|
||||||
|
for config in load_config_list:
|
||||||
|
for info in config:
|
||||||
|
if info["pos"] in unique:
|
||||||
|
continue
|
||||||
|
unique.append(info["pos"])
|
||||||
|
unique.sort()
|
||||||
|
return unique
|
||||||
|
|
||||||
|
def interp1d(self, l1: list[float], l2: list[float], pos: float) -> float:
|
||||||
|
"""1d linear interpolation for the vector components"""
|
||||||
|
fac = (l2[1] - l1[1]) / (l2[0] - l1[0])
|
||||||
|
v = l1[1] + fac * (pos - l1[0])
|
||||||
|
return v
|
||||||
|
|
||||||
|
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int, key: str) -> np.ndarray:
|
||||||
|
"""interpolate the result vectors between load poits"""
|
||||||
|
result = np.zeros(6)
|
||||||
|
for i in range(6):
|
||||||
|
value1 = [loadinfo[start]["pos"], loadinfo[start][key][i]] # [position, force_component]
|
||||||
|
value2 = [loadinfo[end]["pos"], loadinfo[end][key][i]] # [position, force_component]
|
||||||
|
result[i] = self.interp1d(value1, value2, pos) # interpolated [position, force_component]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_before_and_after(self, pos: float, load_config_list: list[list[LoadConfigItem]]) -> dict[str, list[float]]:
|
||||||
|
"""get total values for forces and moments before and after the position
|
||||||
|
example:
|
||||||
|
pos = 2.0
|
||||||
|
load_config_list = [[{"pos":1.0, "descr":"start, "load values":[1,0,0,0,0,0]},
|
||||||
|
{"pos":3.0, "descr":"end, "load values":[3,0,0,0,0,0]}],
|
||||||
|
[{"pos":2.0, "descr":"start, "load values":[1,0,0,0,0,0]},
|
||||||
|
{"pos":3.0, "descr":"end, "load values":[1,0,0,0,0,0]}],
|
||||||
|
[{"pos":1.5, "descr":"start, "load values":[1,0,0,0,0,0]},
|
||||||
|
{"pos":2.5, "descr":"end, "load values":[1,0,0,0,0,0]}],
|
||||||
|
return = {
|
||||||
|
"before": [3,0,0,0,0,0], ->(fx, fy, fz, mx, my, mz)
|
||||||
|
" after": [4,0,0,0,0,0] ->(fx, fy, fz, mx, my, mz)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
load_before = np.zeros(6)
|
||||||
|
load_after = np.zeros(6)
|
||||||
|
|
||||||
|
for config in load_config_list:
|
||||||
|
if pos < config[0]["pos"] or pos > config[-1]["pos"]:
|
||||||
|
continue
|
||||||
|
start = 0
|
||||||
|
end = len(config) - 1
|
||||||
|
while end - start > 0:
|
||||||
|
if pos < config[start]["pos"] or pos > config[end]["pos"]:
|
||||||
|
break
|
||||||
|
if config[start]["pos"] == pos:
|
||||||
|
if config[start]["descr"] in ["start", "middle"]:
|
||||||
|
load_after += config[start]["load values"]
|
||||||
|
|
||||||
|
elif config[start]["descr"] in ["end", "middle"]:
|
||||||
|
load_before += config[start]["load values"]
|
||||||
|
|
||||||
|
elif config[end]["pos"] == pos:
|
||||||
|
if config[end]["descr"] in ["start", "middle"]:
|
||||||
|
load_after += config[end]["load values"]
|
||||||
|
|
||||||
|
elif config[end]["descr"] in ["end", "middle"]:
|
||||||
|
load_before += config[end]["load values"]
|
||||||
|
|
||||||
|
elif end - start == 1:
|
||||||
|
load_before += self.interpolate(pos, config, start, end, "load values")
|
||||||
|
load_after += self.interpolate(pos, config, start, end, "load values")
|
||||||
|
start += 1
|
||||||
|
end -= 1
|
||||||
|
return_value = {"before": load_before.tolist(), "after": load_after.tolist()}
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
def parse_linear_loads_to_dict(
|
||||||
|
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
|
||||||
|
) -> ParsedLoad:
|
||||||
|
"""
|
||||||
|
get load list
|
||||||
|
activity_list: list of IfcStructuralCurveAction or IfcStructuralCurveReaction
|
||||||
|
applied in the structural curve member
|
||||||
|
global_to_local: transformation matrix from global coordinates to local coordinetes
|
||||||
|
return: dict{
|
||||||
|
"constant force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
|
||||||
|
constant distribution
|
||||||
|
"quadratic force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
|
||||||
|
quadratic distribution
|
||||||
|
"sinus force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
|
||||||
|
sinus distribution
|
||||||
|
"linear load configuration": list -> list of load configurations for linear
|
||||||
|
and polyline distributions of linear loads
|
||||||
|
}
|
||||||
|
description of "linear load configuration":
|
||||||
|
list[ -> one item (list)for each IfcStructuralCurveAction applied in the member
|
||||||
|
with IfcStructuralLoadConfiguration as the applied load
|
||||||
|
list[ -> one item (dict) for each item found in the
|
||||||
|
Locations attribute of IfcLoadConfiguration
|
||||||
|
dict{
|
||||||
|
"pos": float, -> local position along curve length
|
||||||
|
"descr": string, -> describe if the item is at the start, middle or end of the list
|
||||||
|
"load values": Array, -> linear force applied at that point
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
constant = np.zeros(6)
|
||||||
|
quadratic = np.zeros(6)
|
||||||
|
sinus = np.zeros(6)
|
||||||
|
linear_load_configurations = []
|
||||||
|
point_load_configurations = []
|
||||||
|
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), "LENGTHUNIT")
|
||||||
|
|
||||||
|
def get_load_values(load, transform_matrix, factor=1.0):
|
||||||
|
result = np.zeros(6)
|
||||||
|
keys = ["LinearForceX", "LinearForceY", "LinearForceZ", "LinearMomentX", "LinearMomentY", "LinearMomentZ"]
|
||||||
|
for i, key in enumerate(keys):
|
||||||
|
value = 0 if getattr(load, key, 0) is None else getattr(load, key, 0)
|
||||||
|
result[i] += value * factor
|
||||||
|
return transform_matrix @ result
|
||||||
|
|
||||||
|
for item in activity_list:
|
||||||
|
activity = item[0]
|
||||||
|
factor = item[1]
|
||||||
|
load = activity.AppliedLoad
|
||||||
|
|
||||||
|
transform_3by3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
|
||||||
|
transform_6by6 = np.zeros((6, 6))
|
||||||
|
transform_6by6[0:3, 0:3] = transform_3by3
|
||||||
|
transform_6by6[3:6, 3:6] = transform_3by3
|
||||||
|
|
||||||
|
# values for linear loads
|
||||||
|
if load.is_a("IfcStructuralLoadConfiguration"):
|
||||||
|
locations = getattr(load, "Locations", [])
|
||||||
|
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadLinearForce"]
|
||||||
|
config_list = []
|
||||||
|
for i, l in enumerate(values):
|
||||||
|
load_values = get_load_values(l, transform_6by6, factor)
|
||||||
|
if i == 0:
|
||||||
|
descr = "start"
|
||||||
|
elif i == len(values) - 1:
|
||||||
|
descr = "end"
|
||||||
|
else:
|
||||||
|
descr = "middle"
|
||||||
|
config_list.append(
|
||||||
|
{"pos": locations[i][0] * unit_scale, "descr": descr, "load values": load_values}
|
||||||
|
)
|
||||||
|
linear_load_configurations.append(config_list)
|
||||||
|
|
||||||
|
# load configurations with point loads
|
||||||
|
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadSingleForce"]
|
||||||
|
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
|
||||||
|
config_list = []
|
||||||
|
for i, val in enumerate(values):
|
||||||
|
result_list = [0, 0, 0, 0, 0, 0]
|
||||||
|
for j, attr in enumerate(attr_list):
|
||||||
|
value = 0 if getattr(val, attr, 0) is None else getattr(val, attr, 0)
|
||||||
|
result_list[j] += value * factor
|
||||||
|
config_list.append({"pos": locations[i][0] * unit_scale, "values": result_list})
|
||||||
|
point_load_configurations.append(config_list)
|
||||||
|
|
||||||
|
else:
|
||||||
|
load_values = get_load_values(load, transform_6by6, factor)
|
||||||
|
if "CONST" == getattr(activity, "PredefinedType", None) or activity.is_a("IfcStructuralLinearAction"):
|
||||||
|
constant += load_values
|
||||||
|
elif "PARABOLA" == getattr(activity, "PredefinedType", None):
|
||||||
|
quadratic += load_values
|
||||||
|
elif "SINUS" == getattr(activity, "PredefinedType", None):
|
||||||
|
sinus += load_values
|
||||||
|
return_value = {
|
||||||
|
"constant force": constant.tolist(),
|
||||||
|
"quadratic force": quadratic.tolist(),
|
||||||
|
"sinus force": sinus.tolist(),
|
||||||
|
"linear load configuration": linear_load_configurations,
|
||||||
|
"point load configuration": point_load_configurations,
|
||||||
|
}
|
||||||
|
return return_value
|
||||||
@@ -27,6 +27,45 @@ import bonsai.tool as tool
|
|||||||
from math import degrees
|
from math import degrees
|
||||||
from mathutils import Vector, Matrix
|
from mathutils import Vector, Matrix
|
||||||
from bonsai.bim.ifc import IfcStore
|
from bonsai.bim.ifc import IfcStore
|
||||||
|
from bonsai.bim.module.structural.decorator import LoadsDecorator
|
||||||
|
|
||||||
|
|
||||||
|
class ShowLoads(bpy.types.Operator):
|
||||||
|
"""Draw decorations to show strucutural actions in 3d view"""
|
||||||
|
|
||||||
|
bl_idname = "bim.show_loads"
|
||||||
|
bl_label = "Show loads in 3D View"
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if event.type == "F5":
|
||||||
|
LoadsDecorator.update()
|
||||||
|
for area in context.screen.areas:
|
||||||
|
if area.type == "VIEW_3D":
|
||||||
|
area.tag_redraw()
|
||||||
|
if event.type == "ESC":
|
||||||
|
LoadsDecorator.uninstall()
|
||||||
|
for area in context.screen.areas:
|
||||||
|
if area.type == "VIEW_3D":
|
||||||
|
area.tag_redraw()
|
||||||
|
return {"FINISHED"}
|
||||||
|
return {"PASS_THROUGH"}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
collection = bpy.data.collections["IfcStructuralItem"]
|
||||||
|
collection.hide_viewport = False
|
||||||
|
context.window.cursor_modal_set("WAIT")
|
||||||
|
try:
|
||||||
|
LoadsDecorator.install(context)
|
||||||
|
except Exception as exc:
|
||||||
|
context.window.cursor_modal_restore()
|
||||||
|
raise exc
|
||||||
|
context.window.cursor_modal_restore()
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
for area in context.screen.areas:
|
||||||
|
if area.type == "VIEW_3D":
|
||||||
|
area.tag_redraw()
|
||||||
|
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
|
||||||
class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
|
class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import bpy
|
|||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from bonsai.bim.ifc import IfcStore
|
from bonsai.bim.ifc import IfcStore
|
||||||
from bonsai.bim.prop import StrProperty, Attribute
|
from bonsai.bim.prop import StrProperty, Attribute
|
||||||
from bonsai.bim.module.structural.data import StructuralLoadCasesData, StructuralLoadsData, BoundaryConditionsData
|
from bonsai.bim.module.structural.data import (
|
||||||
|
StructuralLoadCasesData,
|
||||||
|
StructuralLoadsData,
|
||||||
|
BoundaryConditionsData,
|
||||||
|
LoadGroupDecorationData,
|
||||||
|
)
|
||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
PointerProperty,
|
PointerProperty,
|
||||||
@@ -35,6 +40,16 @@ from bpy.props import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_load_groups_to_show(self, context):
|
||||||
|
if not LoadGroupDecorationData.is_loaded:
|
||||||
|
LoadGroupDecorationData.load()
|
||||||
|
return LoadGroupDecorationData.data["load groups to show"]
|
||||||
|
|
||||||
|
|
||||||
|
def update_activity_type(self, context):
|
||||||
|
LoadGroupDecorationData.is_loaded = False
|
||||||
|
|
||||||
|
|
||||||
def get_applicable_structural_load_types(self, context):
|
def get_applicable_structural_load_types(self, context):
|
||||||
if not StructuralLoadCasesData.is_loaded:
|
if not StructuralLoadCasesData.is_loaded:
|
||||||
StructuralLoadCasesData.load()
|
StructuralLoadCasesData.load()
|
||||||
@@ -152,6 +167,27 @@ class BIMStructuralProperties(PropertyGroup):
|
|||||||
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
|
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
|
||||||
filtered_boundary_conditions: BoolProperty(name="Filtered Boundary Conditions", default=False)
|
filtered_boundary_conditions: BoolProperty(name="Filtered Boundary Conditions", default=False)
|
||||||
|
|
||||||
|
show_loads: BoolProperty(name="Show Loads", default=False)
|
||||||
|
update_load_repr: BoolProperty(name="Update Load Representation", default=False)
|
||||||
|
enable_repr_auto_update: BoolProperty(name="Auto Update", default=False)
|
||||||
|
reference_frame: EnumProperty(
|
||||||
|
items=[
|
||||||
|
("GLOBAL_COORDS", "Global", "Show loads in global reference frame"),
|
||||||
|
("LOCAL_COORDS", "Local", "Show loads in local reference frame"),
|
||||||
|
],
|
||||||
|
name="Reference Frame",
|
||||||
|
)
|
||||||
|
activity_type: EnumProperty(
|
||||||
|
items=[
|
||||||
|
("Action", "Actions", "Show actions loads"),
|
||||||
|
("External Reaction", "External Reactions", "Show reactions on boundary conditions"),
|
||||||
|
("Internal Reactions", "Internal Reactions", "Show internal reactions on members"),
|
||||||
|
],
|
||||||
|
name="Activity Type",
|
||||||
|
update=update_activity_type,
|
||||||
|
)
|
||||||
|
load_group_to_show: EnumProperty(items=get_load_groups_to_show, name="Load Groups")
|
||||||
|
|
||||||
|
|
||||||
class BIMObjectStructuralProperties(PropertyGroup):
|
class BIMObjectStructuralProperties(PropertyGroup):
|
||||||
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
|
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
# Bonsai - OpenBIM Blender Add-on
|
||||||
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of Bonsai.
|
||||||
|
#
|
||||||
|
# Bonsai 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.
|
||||||
|
#
|
||||||
|
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import gpu
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
class DecorationShader:
|
||||||
|
"shader for the load decorations"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def new(
|
||||||
|
self,
|
||||||
|
pattern: Literal[
|
||||||
|
"PERPENDICULAR DISTRIBUTED FORCE",
|
||||||
|
"PARALLEL DISTRIBUTED FORCE",
|
||||||
|
"DISTRIBUTED MOMENT",
|
||||||
|
"SINGLE FORCE",
|
||||||
|
"SINGLE MOMENT",
|
||||||
|
"PLANAR LOAD",
|
||||||
|
],
|
||||||
|
) -> gpu.types.GPUShader:
|
||||||
|
"""pattern: string description of the desired shader
|
||||||
|
Possible values
|
||||||
|
PERPENDICULAR DISTRIBUTED FORCE: pattern for distributed force
|
||||||
|
perpendicular to the curve member axis
|
||||||
|
PARALLEL DISTRIBUTED FORCE: pattern for distributed force
|
||||||
|
along the curve member axis
|
||||||
|
DISTRIBUTED MOMENT: pattern for distributed moment in curve members
|
||||||
|
SINGLE FORCE: pattern for single forces
|
||||||
|
SINGLE MOMENT: pattern for single moments
|
||||||
|
PLANAR LOAD: pattern for planar loads
|
||||||
|
"""
|
||||||
|
valid_patterns = {
|
||||||
|
"PERPENDICULAR DISTRIBUTED FORCE",
|
||||||
|
"PARALLEL DISTRIBUTED FORCE",
|
||||||
|
"DISTRIBUTED MOMENT",
|
||||||
|
"SINGLE FORCE",
|
||||||
|
"SINGLE MOMENT",
|
||||||
|
"PLANAR LOAD",
|
||||||
|
}
|
||||||
|
if pattern not in valid_patterns:
|
||||||
|
raise ValueError(
|
||||||
|
"""pattern must be one of:
|
||||||
|
PERPENDICULAR DISTRIBUTED FORCE
|
||||||
|
PARALLEL DISTRIBUTED FORCE,
|
||||||
|
DISTRIBUTED MOMENT,
|
||||||
|
SINGLE FORCE,
|
||||||
|
SINGLE MOMENT,
|
||||||
|
PLANAR LOAD"""
|
||||||
|
)
|
||||||
|
if "DISTRIBUTED" in pattern.upper():
|
||||||
|
shader = self.get_linear_shader(pattern)
|
||||||
|
return shader
|
||||||
|
elif "SINGLE" in pattern.upper():
|
||||||
|
shader = self.get_point_shader(pattern)
|
||||||
|
return shader
|
||||||
|
elif "PLANAR" in pattern.upper():
|
||||||
|
shader = self.get_planar_shader()
|
||||||
|
return shader
|
||||||
|
|
||||||
|
def get_linear_shader(
|
||||||
|
self, pattern: Literal["PERPENDICULAR DISTRIBUTED FORCE", "PARALLEL DISTRIBUTED FORCE", "DISTRIBUTED MOMENT"]
|
||||||
|
) -> gpu.types.GPUShader:
|
||||||
|
"""pattern: type of pattern
|
||||||
|
PERPENDICULAR DISTRIBUTED FORCE
|
||||||
|
PARALLEL DISTRIBUTED FORCE,
|
||||||
|
DISTRIBUTED MOMENT,
|
||||||
|
"""
|
||||||
|
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||||
|
vert_out.smooth("VEC3", "forces")
|
||||||
|
vert_out.smooth("VEC3", "co")
|
||||||
|
|
||||||
|
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||||
|
shader_info.push_constant("MAT4", "viewProjectionMatrix")
|
||||||
|
shader_info.push_constant("VEC4", "color")
|
||||||
|
shader_info.push_constant("FLOAT", "spacing")
|
||||||
|
shader_info.push_constant("FLOAT", "maxload")
|
||||||
|
|
||||||
|
shader_info.vertex_in(0, "VEC3", "position")
|
||||||
|
shader_info.vertex_in(1, "VEC3", "sin_quad_lin_forces")
|
||||||
|
shader_info.vertex_in(2, "VEC3", "coord")
|
||||||
|
|
||||||
|
shader_info.vertex_out(vert_out)
|
||||||
|
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||||
|
|
||||||
|
shader_info.vertex_source(
|
||||||
|
"void main()"
|
||||||
|
"{"
|
||||||
|
" gl_Position = viewProjectionMatrix * vec4(position, 1.0f);"
|
||||||
|
" co = coord;"
|
||||||
|
" forces = sin_quad_lin_forces;"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if pattern == "PERPENDICULAR DISTRIBUTED FORCE":
|
||||||
|
shader_info.fragment_source(
|
||||||
|
"void main()"
|
||||||
|
"{"
|
||||||
|
"float x = co.x;"
|
||||||
|
"float y = co.y;"
|
||||||
|
"float abs_y = abs(y);"
|
||||||
|
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
|
||||||
|
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
|
||||||
|
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
|
||||||
|
"float sinvalue = forces.x;"
|
||||||
|
"float quadraticvalue = forces.y;"
|
||||||
|
"float linearvalue = forces.z;"
|
||||||
|
"x = co.x/co.z;"
|
||||||
|
"float f = (sin(x*3.1416)*sinvalue"
|
||||||
|
"+(-4.*x*x+4.*x)*quadraticvalue"
|
||||||
|
"+linearvalue)/maxload;"
|
||||||
|
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
|
||||||
|
"float top = step(abs(y-f),0.2*1.2*spacing);"
|
||||||
|
"float d = clamp(top+b+c,0.0,0.9)*mask;"
|
||||||
|
"if (d == 0.0) discard;"
|
||||||
|
"FragColor = vec4(color.xyz,d*color.w);"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif pattern == "PARALLEL DISTRIBUTED FORCE":
|
||||||
|
shader_info.fragment_source(
|
||||||
|
"void main()"
|
||||||
|
"{"
|
||||||
|
"float y = co.y;"
|
||||||
|
"float x = step(0.,y)*(co.z-co.x)+step(y,0.)*(co.x);"
|
||||||
|
"float abs_y = abs(y);"
|
||||||
|
"float a = abs(mod(abs_y,spacing)-0.5*spacing)*5.0;"
|
||||||
|
"float a2 = mod(x,3.0*spacing);"
|
||||||
|
"float b = step(a,a2)*step(a2,1.2*spacing);"
|
||||||
|
"float c = step(0.8*spacing,mod(abs_y+0.4*spacing,spacing))"
|
||||||
|
"*(step(1.2*spacing,a2))*step(a2,2.5*spacing);"
|
||||||
|
"float sinvalue = forces.x;"
|
||||||
|
"float quadraticvalue = forces.y;"
|
||||||
|
"float linearvalue = forces.z;"
|
||||||
|
"x = co.x/co.z;"
|
||||||
|
"float f = (sin(x*3.1416)*sinvalue"
|
||||||
|
"+(-4.*x*x+4.*x)*quadraticvalue"
|
||||||
|
"+linearvalue)/maxload;"
|
||||||
|
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
|
||||||
|
"float top = step(abs(y-f),0.2*1.2*spacing);"
|
||||||
|
"float d = clamp(top+b+c,0.0,0.9)*mask;"
|
||||||
|
"if (d == 0.0) discard;"
|
||||||
|
"FragColor = vec4(color.xyz,d*color.w);"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif pattern == "DISTRIBUTED MOMENT":
|
||||||
|
shader_info.fragment_source(
|
||||||
|
"void main()"
|
||||||
|
"{"
|
||||||
|
"float x = step(co.y,0.)*(co.x)+step(0.,co.y)*(co.z-co.x);"
|
||||||
|
"float y = step(co.y,-0.00001)*(co.y)+step(0.,co.y)*(0.-co.y);"
|
||||||
|
"x = mod((0.5/spacing)*x,1.4)-0.7;"
|
||||||
|
"y = mod((0.5/spacing)*y,1.4)-0.7;"
|
||||||
|
"float abs_y = abs(y);"
|
||||||
|
"vec2 st = vec2(1.9*x,y);"
|
||||||
|
"vec2 orig = vec2(0.,0.);"
|
||||||
|
"float circ = step(distance(st,orig),0.33)*step(0.27,distance(st,orig));"
|
||||||
|
"float tri_mask = step(st.y,st.x)+step(-st.x,st.y);"
|
||||||
|
"float circ_arrow = step(st.x,4.0*st.y-0.75)*step(0.25*st.y-0.34,st.x)*(1.-tri_mask);"
|
||||||
|
"float circmask = step(distance(st,orig),0.1)+step(0.5,distance(st,orig))+step(st.x,0.);"
|
||||||
|
"float body = step(-0.03,st.y)*step(st.y,0.03)*step(-0.3,x)*step(x,0.576);"
|
||||||
|
"float body_arrow = step(-0.5+3.*st.y,x)*step(-0.5-3.*st.y,x)*step(x,-0.3);"
|
||||||
|
"float d = clamp(circmask*(body+body_arrow)+circ_arrow+circ*tri_mask,0.,1.);"
|
||||||
|
"float sinvalue = forces.x;"
|
||||||
|
"float quadraticvalue = forces.y;"
|
||||||
|
"float linearvalue = forces.z;"
|
||||||
|
"x = co.x/co.z;"
|
||||||
|
"float f = (sin(x*3.1416)*sinvalue"
|
||||||
|
"+(-4.*x*x+4.*x)*quadraticvalue"
|
||||||
|
"+linearvalue)/maxload;"
|
||||||
|
"float mask = step(0.,co.y)*step(co.y,f)+step(co.y,0.)*step(f,co.y);"
|
||||||
|
"float top = step(abs(co.y-f),0.2*1.2*spacing);"
|
||||||
|
"d = clamp(top+d,0.0,0.9)*mask;"
|
||||||
|
"if (d == 0.0) discard;"
|
||||||
|
"FragColor = vec4(color.xyz,d*color.w);"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
shader = gpu.shader.create_from_info(shader_info)
|
||||||
|
del vert_out
|
||||||
|
del shader_info
|
||||||
|
return shader
|
||||||
|
|
||||||
|
def get_point_shader(self, pattern: Literal["SINGLE FORCE", "SINGLE MOMENT"]) -> gpu.types.GPUShader:
|
||||||
|
"""param: pattern: type of pattern
|
||||||
|
SINGLE FORCE,
|
||||||
|
SINGLE MOMENT"""
|
||||||
|
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||||
|
vert_out.smooth("VEC3", "co")
|
||||||
|
|
||||||
|
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||||
|
shader_info.push_constant("MAT4", "viewProjectionMatrix")
|
||||||
|
shader_info.push_constant("VEC4", "color")
|
||||||
|
shader_info.push_constant("FLOAT", "spacing")
|
||||||
|
|
||||||
|
shader_info.vertex_in(0, "VEC3", "position")
|
||||||
|
shader_info.vertex_in(1, "VEC3", "coord")
|
||||||
|
|
||||||
|
shader_info.vertex_out(vert_out)
|
||||||
|
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||||
|
|
||||||
|
shader_info.vertex_source(
|
||||||
|
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if pattern == "SINGLE FORCE":
|
||||||
|
shader_info.fragment_source(
|
||||||
|
"void main()"
|
||||||
|
"{"
|
||||||
|
"float body = step(abs(co.x),0.2*spacing)*step(2.*spacing,co.y);"
|
||||||
|
"float arrow = step(3.5*abs(co.x)+0.02,abs(co.y))*step(co.y,2.*spacing)*step(0.,co.y);"
|
||||||
|
"float d = clamp(body+arrow,0.0,0.5);"
|
||||||
|
"if (d == 0.0) discard;"
|
||||||
|
"FragColor = vec4(color.xyz,d*color.w);"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif pattern == "SINGLE MOMENT":
|
||||||
|
shader_info.fragment_source(
|
||||||
|
"void main()"
|
||||||
|
"{"
|
||||||
|
"float circ = step(distance(co.xy,vec2(0.,0.)),0.33)*step(0.27,distance(co.xy,vec2(0.,0.)));"
|
||||||
|
"float mask = step(co.y,co.x)+step(-co.x,co.y);"
|
||||||
|
"float circ_arrow = step(co.x,4.0*co.y-0.75)*step(0.25*co.y-0.34,co.x)*(1.-mask);"
|
||||||
|
"float d = clamp(circ_arrow+circ*mask,0.0,0.5);"
|
||||||
|
"if (d == 0.0) discard;"
|
||||||
|
"FragColor = vec4(color.xyz,d*color.w);"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
shader = gpu.shader.create_from_info(shader_info)
|
||||||
|
del vert_out
|
||||||
|
del shader_info
|
||||||
|
return shader
|
||||||
|
|
||||||
|
def get_planar_shader(self) -> gpu.types.GPUShader:
|
||||||
|
"""shader for planar loads"""
|
||||||
|
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||||
|
vert_out.smooth("VEC3", "co")
|
||||||
|
|
||||||
|
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||||
|
shader_info.push_constant("MAT4", "viewProjectionMatrix")
|
||||||
|
shader_info.push_constant("VEC4", "color")
|
||||||
|
shader_info.push_constant("FLOAT", "spacing")
|
||||||
|
|
||||||
|
shader_info.vertex_in(0, "VEC3", "position")
|
||||||
|
shader_info.vertex_in(1, "VEC3", "coord")
|
||||||
|
|
||||||
|
shader_info.vertex_out(vert_out)
|
||||||
|
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||||
|
|
||||||
|
shader_info.vertex_source(
|
||||||
|
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
|
||||||
|
)
|
||||||
|
|
||||||
|
shader_info.fragment_source(
|
||||||
|
"void main()"
|
||||||
|
"{"
|
||||||
|
"float x = co.x;"
|
||||||
|
"float y = co.y;"
|
||||||
|
"float abs_y = abs(y);"
|
||||||
|
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
|
||||||
|
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
|
||||||
|
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
|
||||||
|
"float mask = step(0.,y)*step(y,0.98)+step(y,0.)*step(0.98,y);"
|
||||||
|
"float top = step(abs(y-0.98),0.2*1.2*spacing);"
|
||||||
|
"float d = clamp(0.2*y+(top+b+c)*mask,0.0,0.4);"
|
||||||
|
"FragColor = vec4(color.xyz,d*color.w);"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
shader = gpu.shader.create_from_info(shader_info)
|
||||||
|
del vert_out
|
||||||
|
del shader_info
|
||||||
|
return shader
|
||||||
@@ -445,6 +445,37 @@ class BIM_UL_structural_activities(UIList):
|
|||||||
row.label(text=item.applied_load_class)
|
row.label(text=item.applied_load_class)
|
||||||
|
|
||||||
|
|
||||||
|
class BIM_PT_show_structural_activities(Panel):
|
||||||
|
bl_label = "Show Loads"
|
||||||
|
bl_idname = "BIM_PT_show_structural_activities"
|
||||||
|
bl_space_type = "PROPERTIES"
|
||||||
|
bl_region_type = "WINDOW"
|
||||||
|
bl_context = "scene"
|
||||||
|
bl_parent_id = "BIM_PT_tab_structural"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
file = IfcStore.get_file()
|
||||||
|
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
|
||||||
|
self.props = context.scene.BIMStructuralProperties
|
||||||
|
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.operator(
|
||||||
|
"bim.show_loads",
|
||||||
|
text="Show loads",
|
||||||
|
icon="HIDE_OFF",
|
||||||
|
)
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.prop(self.props, "reference_frame")
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.prop(self.props, "activity_type")
|
||||||
|
row = self.layout.row(align=True)
|
||||||
|
row.prop(self.props, "load_group_to_show")
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_structural_loads(Panel):
|
class BIM_PT_structural_loads(Panel):
|
||||||
bl_label = "Structural Loads"
|
bl_label = "Structural Loads"
|
||||||
bl_idname = "BIM_PT_structural_loads"
|
bl_idname = "BIM_PT_structural_loads"
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ class RemoveStyle(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
style: bpy.props.IntProperty()
|
style: bpy.props.IntProperty()
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
core.disable_editing_style(tool.Style) # So we don't end up soft-locking style edition
|
|
||||||
core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True)
|
core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
related_object: bpy.props.StringProperty()
|
related_object: bpy.props.StringProperty()
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
type = tool.Ifc.get().by_id(self.relating_type or int(context.active_object.BIMTypeProperties.relating_type))
|
relating_type = tool.Ifc.get().by_id(
|
||||||
|
self.relating_type or int(context.active_object.BIMTypeProperties.relating_type)
|
||||||
|
)
|
||||||
related_objects = (
|
related_objects = (
|
||||||
[bpy.data.objects.get(self.related_object)]
|
[bpy.data.objects.get(self.related_object)]
|
||||||
if self.related_object
|
if self.related_object
|
||||||
@@ -49,9 +51,9 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
model_props = context.scene.BIMModelProperties
|
model_props = context.scene.BIMModelProperties
|
||||||
for obj in related_objects:
|
for obj in related_objects:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
core.assign_type(tool.Ifc, tool.Type, element=element, type=type)
|
core.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||||
if model_props.occurrence_name_style == "TYPE":
|
if model_props.occurrence_name_style == "TYPE":
|
||||||
obj.name = tool.Model.generate_occurrence_name(type, element.is_a())
|
obj.name = tool.Model.generate_occurrence_name(relating_type, element.is_a())
|
||||||
|
|
||||||
|
|
||||||
class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
|
class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
|||||||
activate_workspace: BoolProperty(
|
activate_workspace: BoolProperty(
|
||||||
name="Activate BIM Workspace on Startup",
|
name="Activate BIM Workspace on Startup",
|
||||||
default=True,
|
default=True,
|
||||||
description="If enabled, this will automatically activate the BIM workspace when opening a project.\It is recommended to keep this `Enabled`",
|
description="If enabled, this will automatically activate the BIM workspace when opening a project.\nIt is recommended to keep this `Enabled`",
|
||||||
)
|
)
|
||||||
should_setup_toolbar: BoolProperty(
|
should_setup_toolbar: BoolProperty(
|
||||||
name="Always Show Toolbar In 3D Viewport",
|
name="Always Show Toolbar In 3D Viewport",
|
||||||
@@ -246,9 +246,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
|||||||
)
|
)
|
||||||
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
|
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
|
||||||
spatial_elements_unselectable: BoolProperty(
|
spatial_elements_unselectable: BoolProperty(
|
||||||
name="Make Spatial Elements Unselectable By Default",
|
name="Make Spatial Elements Unselectable By Default",
|
||||||
default=True,
|
default=True,
|
||||||
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences")
|
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences",
|
||||||
|
)
|
||||||
decorations_colour: bpy.props.FloatVectorProperty(
|
decorations_colour: bpy.props.FloatVectorProperty(
|
||||||
name="Decorations Color", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
name="Decorations Color", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||||
)
|
)
|
||||||
@@ -1191,15 +1192,16 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
|
|||||||
url_op = layout.operator("bim.open_uri", icon="URL", text="Online IFC Documentation")
|
url_op = layout.operator("bim.open_uri", icon="URL", text="Online IFC Documentation")
|
||||||
url_op.uri = url
|
url_op.uri = url
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_decorators_overlay(Panel):
|
class BIM_PT_decorators_overlay(Panel):
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = "VIEW_3D"
|
||||||
bl_region_type = 'HEADER'
|
bl_region_type = "HEADER"
|
||||||
bl_parent_id = 'VIEW3D_PT_overlay'
|
bl_parent_id = "VIEW3D_PT_overlay"
|
||||||
bl_label = "Bonsai Decorators"
|
bl_label = "Bonsai Decorators"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.mode == 'OBJECT'
|
return context.mode == "OBJECT"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|||||||
@@ -86,12 +86,17 @@ def add_part_to_object(
|
|||||||
assign_object(ifc, aggregator, collector, relating_obj=obj, related_obj=part_obj)
|
assign_object(ifc, aggregator, collector, relating_obj=obj, related_obj=part_obj)
|
||||||
blender.set_active_object(obj)
|
blender.set_active_object(obj)
|
||||||
|
|
||||||
def enable_aggregate_mode(aggregator: tool.Aggregate, obj: bpy.types.Object,):
|
|
||||||
|
def enable_aggregate_mode(
|
||||||
|
aggregator: tool.Aggregate,
|
||||||
|
obj: bpy.types.Object,
|
||||||
|
):
|
||||||
if aggregator.get_aggregate_mode():
|
if aggregator.get_aggregate_mode():
|
||||||
disable_aggregate_mode(aggregator)
|
disable_aggregate_mode(aggregator)
|
||||||
|
|
||||||
aggregator.enable_aggregate_mode(obj)
|
aggregator.enable_aggregate_mode(obj)
|
||||||
|
|
||||||
|
|
||||||
def disable_aggregate_mode(aggregator: tool.Aggregate):
|
def disable_aggregate_mode(aggregator: tool.Aggregate):
|
||||||
aggregator.disable_aggregate_mode()
|
aggregator.disable_aggregate_mode()
|
||||||
|
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ def add_annotation(
|
|||||||
ifc_representation_class=drawing_tool.get_ifc_representation_class(object_type),
|
ifc_representation_class=drawing_tool.get_ifc_representation_class(object_type),
|
||||||
)
|
)
|
||||||
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), products=[element])
|
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), products=[element])
|
||||||
collector.assign(obj)
|
collector.assign(obj, should_clean_users_collection=True)
|
||||||
drawing_tool.enable_editing(obj)
|
drawing_tool.enable_editing(obj)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def copy_class(
|
def copy_class(
|
||||||
ifc: tool.Ifc, collector: tool.Collector, geometry: tool.Geometry, root: tool.Root, obj: bpy.types.Object
|
ifc: tool.Ifc, collector: tool.Collector, geometry: tool.Geometry, root: tool.Root, obj: bpy.types.Object
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance | None:
|
||||||
element = ifc.get_entity(obj)
|
element = ifc.get_entity(obj)
|
||||||
if not element:
|
if not element:
|
||||||
return
|
return
|
||||||
@@ -43,14 +43,12 @@ def copy_class(
|
|||||||
ifc.run("type.map_type_representations", related_object=new, relating_type=relating_type)
|
ifc.run("type.map_type_representations", related_object=new, relating_type=relating_type)
|
||||||
root.link_object_data(ifc.get_object(relating_type), obj)
|
root.link_object_data(ifc.get_object(relating_type), obj)
|
||||||
elif representation:
|
elif representation:
|
||||||
root.copy_representation(element, new)
|
copied_entities = root.copy_representation(element, new)
|
||||||
new_representation = root.get_element_representation(new, root.get_representation_context(representation))
|
|
||||||
data = geometry.duplicate_object_data(obj)
|
data = geometry.duplicate_object_data(obj)
|
||||||
if data:
|
if data:
|
||||||
|
geometry.copy_data_links(data, copied_entities)
|
||||||
geometry.change_object_data(obj, data, is_global=True)
|
geometry.change_object_data(obj, data, is_global=True)
|
||||||
geometry.rename_object(data, geometry.get_representation_name(new_representation))
|
geometry.rename_object(data, geometry.get_representation_name(ifc.get_entity(data)))
|
||||||
geometry.link(new_representation, data)
|
|
||||||
geometry.reload_representation_item_ids(new_representation, data)
|
|
||||||
root.assign_body_styles(new, obj)
|
root.assign_body_styles(new, obj)
|
||||||
collector.assign(obj)
|
collector.assign(obj)
|
||||||
if root.is_element_a(new, "IfcOpeningElement"):
|
if root.is_element_a(new, "IfcOpeningElement"):
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ def remove_style(
|
|||||||
avoid unnecessary reloads.
|
avoid unnecessary reloads.
|
||||||
"""
|
"""
|
||||||
obj = ifc.get_object(style)
|
obj = ifc.get_object(style)
|
||||||
|
if style_tool.is_editing_style() and obj == style_tool.get_currently_edited_material():
|
||||||
|
style_tool.disable_editing()
|
||||||
# Get style_type before removing object as later StylesData might fail to load
|
# Get style_type before removing object as later StylesData might fail to load
|
||||||
# due object not yet removed completely.
|
# due object not yet removed completely.
|
||||||
style_type = style_tool.get_active_style_type()
|
style_type = style_tool.get_active_style_type()
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ class Geometry:
|
|||||||
def clear_cache(cls, element): pass
|
def clear_cache(cls, element): pass
|
||||||
def clear_modifiers(cls, obj): pass
|
def clear_modifiers(cls, obj): pass
|
||||||
def clear_scale(cls, obj): pass
|
def clear_scale(cls, obj): pass
|
||||||
|
def copy_data_links(cls, data, copied_entities) -> None: pass
|
||||||
def delete_data(cls, data): pass
|
def delete_data(cls, data): pass
|
||||||
def delete_ifc_object(cls, obj): pass
|
def delete_ifc_object(cls, obj): pass
|
||||||
def delete_opening_object_placement(cls, opening): pass
|
def delete_opening_object_placement(cls, opening): pass
|
||||||
@@ -420,7 +421,6 @@ class Geometry:
|
|||||||
def record_object_position(cls, obj): pass
|
def record_object_position(cls, obj): pass
|
||||||
def recreate_object_with_data(cls, obj, data): pass
|
def recreate_object_with_data(cls, obj, data): pass
|
||||||
def reimport_element_representations(cls, obj, representation, apply_openings=True): pass
|
def reimport_element_representations(cls, obj, representation, apply_openings=True): pass
|
||||||
def reload_representation_item_ids(cls, representation, data) -> None: pass
|
|
||||||
def remove_connection(cls, connection): pass
|
def remove_connection(cls, connection): pass
|
||||||
def rename_object(cls, obj, name): pass
|
def rename_object(cls, obj, name): pass
|
||||||
def replace_object_data_globally(cls, old_data, new_data): pass
|
def replace_object_data_globally(cls, old_data, new_data): pass
|
||||||
@@ -1013,6 +1013,7 @@ class Style:
|
|||||||
def get_uv_maps(cls, representation): pass
|
def get_uv_maps(cls, representation): pass
|
||||||
def import_presentation_styles(cls, style_type): pass
|
def import_presentation_styles(cls, style_type): pass
|
||||||
def import_surface_attributes(cls, style): pass
|
def import_surface_attributes(cls, style): pass
|
||||||
|
def is_editing_style(cls): pass
|
||||||
def is_editing_styles(cls): pass
|
def is_editing_styles(cls): pass
|
||||||
def reload_material_from_ifc(cls, obj): pass
|
def reload_material_from_ifc(cls, obj): pass
|
||||||
def is_style_side_attribute_edited(cls, style, new_attributes): pass
|
def is_style_side_attribute_edited(cls, style, new_attributes): pass
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_active_object(cls) -> bpy.types.Object:
|
def get_active_object(cls) -> bpy.types.Object:
|
||||||
return bpy.context.view_layer.objects.active
|
return getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_selected_objects(cls) -> set[bpy.types.Object]:
|
def get_selected_objects(cls) -> set[bpy.types.Object]:
|
||||||
@@ -185,10 +185,6 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
return context.scene.BIMMaterialProperties.materials[
|
return context.scene.BIMMaterialProperties.materials[
|
||||||
context.scene.BIMMaterialProperties.active_material_index
|
context.scene.BIMMaterialProperties.active_material_index
|
||||||
].ifc_definition_id
|
].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":
|
elif obj_type == "MaterialSetItem":
|
||||||
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
|
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
|
||||||
elif obj_type == "Task":
|
elif obj_type == "Task":
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ class Collector(bonsai.core.tool.Collector):
|
|||||||
for users_collection in obj.users_collection:
|
for users_collection in obj.users_collection:
|
||||||
if obj.BIMObjectProperties.collection == users_collection:
|
if obj.BIMObjectProperties.collection == users_collection:
|
||||||
continue
|
continue
|
||||||
# Users are free to user extra collections for their own
|
# Users are free to use extra collections for their own
|
||||||
# purposes except for the reserved keyword "Ifc" and
|
# purposes except for the reserved keyword "Ifc" and
|
||||||
# "Collection" (which is the default collection that comes with
|
# "Collection" (which is the default collection that comes with
|
||||||
# a Blender session)
|
# a Blender session) and "Unsorted" (our special collection).
|
||||||
if "Ifc" in users_collection.name or users_collection.name == "Collection":
|
if "Ifc" in users_collection.name or users_collection.name in ("Collection", "Unsorted"):
|
||||||
users_collection.objects.unlink(obj)
|
users_collection.objects.unlink(obj)
|
||||||
|
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
@@ -43,6 +43,7 @@ class Collector(bonsai.core.tool.Collector):
|
|||||||
|
|
||||||
# Note that tool.Geometry.is_locked is only checked within the if
|
# Note that tool.Geometry.is_locked is only checked within the if
|
||||||
# statements for efficiency as it is a slow check.
|
# statements for efficiency as it is a slow check.
|
||||||
|
tool.Geometry.lock_scale(obj)
|
||||||
|
|
||||||
if element.is_a("IfcGridAxis"):
|
if element.is_a("IfcGridAxis"):
|
||||||
if tool.Geometry.is_locked(element):
|
if tool.Geometry.is_locked(element):
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
obj.lock_rotation = (True, True, True)
|
obj.lock_rotation = (True, True, True)
|
||||||
obj.lock_rotation_w = True
|
obj.lock_rotation_w = True
|
||||||
obj.lock_rotations_4d = True
|
obj.lock_rotations_4d = True
|
||||||
obj.lock_scale = (True, True, True)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unlock_object(cls, obj: bpy.types.Object) -> None:
|
def unlock_object(cls, obj: bpy.types.Object) -> None:
|
||||||
@@ -140,7 +139,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
obj.lock_rotation = (False, False, False)
|
obj.lock_rotation = (False, False, False)
|
||||||
obj.lock_rotation_w = False
|
obj.lock_rotation_w = False
|
||||||
obj.lock_rotations_4d = False
|
obj.lock_rotations_4d = False
|
||||||
obj.lock_scale = (False, False, False)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def lock_scale(cls, obj: bpy.types.Object) -> None:
|
def lock_scale(cls, obj: bpy.types.Object) -> None:
|
||||||
@@ -259,10 +257,14 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
# a cylinder) so dissolving edges should not be allowed.
|
# a cylinder) so dissolving edges should not be allowed.
|
||||||
mesh_element = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
mesh_element = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||||
if (
|
if (
|
||||||
mesh_element.is_a("IfcShapeRepresentation")
|
(
|
||||||
and ifcopenshell.util.representation.resolve_representation(mesh_element).RepresentationType
|
mesh_element.is_a("IfcShapeRepresentation")
|
||||||
== "AdvancedBrep"
|
and ifcopenshell.util.representation.resolve_representation(mesh_element).RepresentationType
|
||||||
) or mesh_element.is_a("IfcAdvancedBrep") or not obj.data:
|
== "AdvancedBrep"
|
||||||
|
)
|
||||||
|
or mesh_element.is_a("IfcAdvancedBrep")
|
||||||
|
or not obj.data
|
||||||
|
):
|
||||||
return
|
return
|
||||||
if hasattr(obj.data, "attributes") and (ios_edges_attribute := obj.data.attributes.get("ios_edges")):
|
if hasattr(obj.data, "attributes") and (ios_edges_attribute := obj.data.attributes.get("ios_edges")):
|
||||||
# Edges from a forced triangulation are stored as True in a boolean attribute on the mesh
|
# Edges from a forced triangulation are stored as True in a boolean attribute on the mesh
|
||||||
@@ -612,7 +614,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_representation_name(cls, representation: ifcopenshell.entity_instance) -> str:
|
def get_representation_name(cls, representation: ifcopenshell.entity_instance) -> str:
|
||||||
return tool.Loader.get_mesh_name(representation.ContextOfItems.id(), representation.id())
|
return tool.Loader.get_mesh_name(representation)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_styles(
|
def get_styles(
|
||||||
@@ -719,7 +721,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
if not tool.Loader.is_native_swept_disk_solid(element, representation):
|
if not tool.Loader.is_native_swept_disk_solid(element, representation):
|
||||||
continue
|
continue
|
||||||
if curve is None:
|
if curve is None:
|
||||||
mesh_name = tool.Loader.get_mesh_name(context.id(), representation.id())
|
mesh_name = tool.Loader.get_mesh_name(representation)
|
||||||
native_data = {
|
native_data = {
|
||||||
"representation": representation,
|
"representation": representation,
|
||||||
# TODO: calculate mapped item matrix.
|
# TODO: calculate mapped item matrix.
|
||||||
@@ -1729,8 +1731,14 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reload_representation_item_ids(cls, representation: ifcopenshell.entity_instance, data: bpy.types.Mesh) -> None:
|
def copy_data_links(cls, data: bpy.types.Mesh, copied_entities: dict[int, ifcopenshell.entity_instance]) -> None:
|
||||||
data["ios_item_ids"] = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)]
|
representation = tool.Ifc.get_entity(data)
|
||||||
|
representation = copied_entities.get(representation.id(), representation)
|
||||||
|
tool.Ifc.link(representation, data)
|
||||||
|
if item_ids := data.get("ios_item_ids"):
|
||||||
|
data["ios_item_ids"] = [copied_entities.get(i, tool.Ifc.get().by_id(i)).id() for i in item_ids]
|
||||||
|
if item_ids := data.get("ios_edges_item_ids"):
|
||||||
|
data["ios_edges_item_ids"] = [copied_entities.get(i, tool.Ifc.get().by_id(i)).id() for i in item_ids]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_mesh_to_tessellation(
|
def export_mesh_to_tessellation(
|
||||||
@@ -1795,9 +1803,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_bvh_tree(cls, obj:bpy.types.Object) -> BVHTree:
|
def get_bvh_tree(cls, obj: bpy.types.Object) -> BVHTree:
|
||||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
||||||
bm.transform(obj.matrix_world)
|
bm.transform(obj.matrix_world)
|
||||||
return BVHTree.FromBMesh(bm)
|
return BVHTree.FromBMesh(bm)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ class Ifc(bonsai.core.tool.Ifc):
|
|||||||
OBJECT_TYPE = Literal[
|
OBJECT_TYPE = Literal[
|
||||||
"Object",
|
"Object",
|
||||||
"Material",
|
"Material",
|
||||||
"MaterialSet",
|
|
||||||
"MaterialSetItem",
|
"MaterialSetItem",
|
||||||
"Task",
|
"Task",
|
||||||
"Cost",
|
"Cost",
|
||||||
@@ -102,7 +101,7 @@ class Ifc(bonsai.core.tool.Ifc):
|
|||||||
Return None if object is not linked to IFC or it's linked to non-existent element.
|
Return None if object is not linked to IFC or it's linked to non-existent element.
|
||||||
"""
|
"""
|
||||||
ifc = IfcStore.get_file()
|
ifc = IfcStore.get_file()
|
||||||
if not ifc:
|
if not ifc or not obj:
|
||||||
return
|
return
|
||||||
|
|
||||||
props = None
|
props = None
|
||||||
@@ -110,6 +109,8 @@ class Ifc(bonsai.core.tool.Ifc):
|
|||||||
props = obj.BIMObjectProperties
|
props = obj.BIMObjectProperties
|
||||||
elif isinstance(obj, bpy.types.Material):
|
elif isinstance(obj, bpy.types.Material):
|
||||||
props = obj.BIMStyleProperties
|
props = obj.BIMStyleProperties
|
||||||
|
else:
|
||||||
|
props = obj.BIMMeshProperties
|
||||||
|
|
||||||
if props and (ifc_definition_id := props.ifc_definition_id):
|
if props and (ifc_definition_id := props.ifc_definition_id):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -84,13 +84,12 @@ class Loader(bonsai.core.tool.Loader):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_mesh_name_from_shape(cls, geometry: ifcopenshell.geom.ShapeType) -> str:
|
def get_mesh_name_from_shape(cls, geometry: ifcopenshell.geom.ShapeType) -> str:
|
||||||
representation_id = cls.get_representation_id_from_shape(geometry)
|
representation_id = cls.get_representation_id_from_shape(geometry)
|
||||||
representation = tool.Ifc.get().by_id(representation_id)
|
return cls.get_mesh_name(tool.Ifc.get().by_id(representation_id))
|
||||||
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
|
|
||||||
return cls.get_mesh_name(context_id, representation_id)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_mesh_name(cls, context_id: int, representation_id: int) -> str:
|
def get_mesh_name(cls, representation: ifcopenshell.entity_instance) -> str:
|
||||||
return "{}/{}".format(context_id, representation_id)
|
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
|
||||||
|
return "{}/{}".format(context_id, representation.id())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_name(cls, element: ifcopenshell.entity_instance) -> str:
|
def get_name(cls, element: ifcopenshell.entity_instance) -> str:
|
||||||
@@ -983,8 +982,7 @@ class Loader(bonsai.core.tool.Loader):
|
|||||||
tool.Loader.load_indexed_colour_map(rep, mesh)
|
tool.Loader.load_indexed_colour_map(rep, mesh)
|
||||||
|
|
||||||
ios_edges_values = [
|
ios_edges_values = [
|
||||||
(e.vertices[0], e.vertices[1]) in ios_edges
|
(e.vertices[0], e.vertices[1]) in ios_edges or (e.vertices[1], e.vertices[0]) in ios_edges
|
||||||
or (e.vertices[1], e.vertices[0]) in ios_edges
|
|
||||||
for e in mesh.edges
|
for e in mesh.edges
|
||||||
]
|
]
|
||||||
tool.Blender.Attribute.fill_attribute(mesh, "ios_edges", "EDGE", "BOOLEAN", ios_edges_values)
|
tool.Blender.Attribute.fill_attribute(mesh, "ios_edges", "EDGE", "BOOLEAN", ios_edges_values)
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ class Model(bonsai.core.tool.Model):
|
|||||||
points.append(cls.convert_si_to_unit(list(local_point)))
|
points.append(cls.convert_si_to_unit(list(local_point)))
|
||||||
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_annotation_fill_area(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance | None:
|
||||||
|
result = cls.auto_detect_annotation_fill_area(obj, obj.data)
|
||||||
|
if isinstance(result, dict) and result["annotation_fill_area"]:
|
||||||
|
return tool.Ifc.get().add(result["annotation_fill_area"])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_profile(
|
def export_profile(
|
||||||
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
|
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
|
||||||
@@ -268,6 +274,12 @@ class Model(bonsai.core.tool.Model):
|
|||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def import_annotation_fill_area(
|
||||||
|
cls, annotation_fill_area: ifcopenshell.entity_instance, obj: Optional[bpy.types.Object] = None
|
||||||
|
) -> bpy.types.Object:
|
||||||
|
return cls.import_profile(annotation_fill_area, obj)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_profile(
|
def import_profile(
|
||||||
cls,
|
cls,
|
||||||
@@ -299,6 +311,10 @@ class Model(bonsai.core.tool.Model):
|
|||||||
cls.convert_curve_to_mesh(obj, position, inner_curve)
|
cls.convert_curve_to_mesh(obj, position, inner_curve)
|
||||||
elif profile.is_a() == "IfcRectangleProfileDef":
|
elif profile.is_a() == "IfcRectangleProfileDef":
|
||||||
cls.import_rectangle(obj, position, profile)
|
cls.import_rectangle(obj, position, profile)
|
||||||
|
elif profile.is_a() == "IfcAnnotationFillArea":
|
||||||
|
cls.convert_curve_to_mesh(obj, position, profile.OuterBoundary)
|
||||||
|
for inner_boundary in profile.InnerBoundaries or []:
|
||||||
|
cls.convert_curve_to_mesh(obj, position, inner_boundary)
|
||||||
|
|
||||||
mesh = bpy.data.meshes.new("Profile")
|
mesh = bpy.data.meshes.new("Profile")
|
||||||
mesh.from_pydata(cls.vertices, cls.edges, [])
|
mesh.from_pydata(cls.vertices, cls.edges, [])
|
||||||
@@ -1247,8 +1263,12 @@ class Model(bonsai.core.tool.Model):
|
|||||||
|
|
||||||
if stair_type == "WOOD/STEEL":
|
if stair_type == "WOOD/STEEL":
|
||||||
builder = ShapeBuilder(None)
|
builder = ShapeBuilder(None)
|
||||||
|
|
||||||
# full tread rectangle
|
# full tread rectangle
|
||||||
get_tread_verts = partial(builder.get_rectangle_coords, position=V_(0, -(tread_depth - tread_rise)))
|
def get_tread_verts(*args, **kwargs):
|
||||||
|
fn = partial(builder.get_rectangle_coords, position=V_(0, -(tread_depth - tread_rise)))
|
||||||
|
return [Vector(x) for x in fn(*args, **kwargs)]
|
||||||
|
|
||||||
default_tread_verts = get_tread_verts(size=V_(tread_run + nosing_overlap, tread_depth))
|
default_tread_verts = get_tread_verts(size=V_(tread_run + nosing_overlap, tread_depth))
|
||||||
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
|
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
|
||||||
|
|
||||||
@@ -1575,6 +1595,20 @@ class Model(bonsai.core.tool.Model):
|
|||||||
)
|
)
|
||||||
tool.Model.replace_object_ifc_representation(body, obj, representation)
|
tool.Model.replace_object_ifc_representation(body, obj, representation)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def auto_detect_annotation_fill_area(cls, obj: bpy.types.Object, mesh: bpy.types.Mesh) -> dict | None:
|
||||||
|
result = cls.auto_detect_profiles(obj, mesh)
|
||||||
|
fill_area = None
|
||||||
|
if isinstance(result, dict) and (profile_def := result["profile_def"]):
|
||||||
|
if profile_def.is_a("IfcArbitraryClosedProfileDef"):
|
||||||
|
fill_area = result["ifc_file"].createIfcAnnotationFillArea(profile_def.OuterCurve)
|
||||||
|
elif profile_def.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||||
|
fill_area = result["ifc_file"].createIfcAnnotationFillArea(
|
||||||
|
profile_def.OuterCurve, profile_def.InnerCurves
|
||||||
|
)
|
||||||
|
if fill_area:
|
||||||
|
return {"ifc_file": result["ifc_file"], "annotation_fill_area": fill_area}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def auto_detect_profiles(
|
def auto_detect_profiles(
|
||||||
cls, obj: bpy.types.Object, mesh: bpy.types.Mesh, position: Matrix | None = None
|
cls, obj: bpy.types.Object, mesh: bpy.types.Mesh, position: Matrix | None = None
|
||||||
@@ -1979,3 +2013,18 @@ class Model(bonsai.core.tool.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_booleaned_obj(cls, boolean_obj: bpy.types.Object) -> bpy.types.Object:
|
def get_booleaned_obj(cls, boolean_obj: bpy.types.Object) -> bpy.types.Object:
|
||||||
return boolean_obj.data.BIMMeshProperties.obj
|
return boolean_obj.data.BIMMeshProperties.obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def bm_sort_out_geom(
|
||||||
|
cls, geom_data: list[Union[bmesh.types.BMVert, bmesh.types.BMEdge, bmesh.types.BMFace]]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
geom_dict = {"verts": [], "edges": [], "faces": []}
|
||||||
|
|
||||||
|
for el in geom_data:
|
||||||
|
if isinstance(el, bmesh.types.BMVert):
|
||||||
|
geom_dict["verts"].append(el)
|
||||||
|
elif isinstance(el, bmesh.types.BMFace):
|
||||||
|
geom_dict["faces"].append(el)
|
||||||
|
else:
|
||||||
|
geom_dict["edges"].append(el)
|
||||||
|
return geom_dict
|
||||||
|
|||||||
@@ -33,10 +33,8 @@ class Patch(bonsai.core.tool.Patch):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_filepath_argument(cls, recipe: str, arg_name: str) -> bool:
|
def is_filepath_argument(cls, recipe: str, arg_name: str) -> bool:
|
||||||
# TODO: Temporary hack to identify filepath arguments.
|
# There is probably a more explicit way to do this
|
||||||
# Should mark them as such in the patches documentation
|
return "filepath" in arg_name
|
||||||
# and process it later.
|
|
||||||
return recipe == "SplitByBuildingStorey" and arg_name == "output_dir"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def does_patch_has_output(cls, recipe: str) -> bool:
|
def does_patch_has_output(cls, recipe: str) -> bool:
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ class Pset(bonsai.core.tool.Pset):
|
|||||||
return bpy.data.objects.get(obj).PsetProperties
|
return bpy.data.objects.get(obj).PsetProperties
|
||||||
elif obj_type == "Material":
|
elif obj_type == "Material":
|
||||||
return bpy.context.scene.MaterialPsetProperties
|
return bpy.context.scene.MaterialPsetProperties
|
||||||
elif obj_type == "MaterialSet":
|
|
||||||
return bpy.data.objects.get(obj).MaterialSetPsetProperties
|
|
||||||
elif obj_type == "MaterialSetItem":
|
elif obj_type == "MaterialSetItem":
|
||||||
return bpy.data.objects.get(obj).MaterialSetItemPsetProperties
|
return bpy.data.objects.get(obj).MaterialSetItemPsetProperties
|
||||||
elif obj_type == "Task":
|
elif obj_type == "Task":
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
|
|
||||||
bm.free()
|
bm.free()
|
||||||
snapping_points = []
|
snapping_points = []
|
||||||
sorted_points = sorted(points)
|
sorted_points = sorted(points, key=lambda x: x[0])
|
||||||
for p in sorted_points:
|
for p in sorted_points:
|
||||||
point = copy.deepcopy(p)
|
point = copy.deepcopy(p)
|
||||||
snapping_points.append(point[1])
|
snapping_points.append(point[1])
|
||||||
|
|||||||
@@ -55,28 +55,38 @@ class Root(bonsai.core.tool.Root):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy_representation(cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance) -> None:
|
def copy_representation(
|
||||||
|
cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance
|
||||||
|
) -> dict[int, ifcopenshell.entity_instance]:
|
||||||
def exclude_callback(attribute):
|
def exclude_callback(attribute):
|
||||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||||
|
|
||||||
|
copied_entities: dict[int, ifcopenshell.entity_instance] = {}
|
||||||
|
|
||||||
if dest.is_a("IfcProduct"):
|
if dest.is_a("IfcProduct"):
|
||||||
if not source.Representation:
|
if not source.Representation:
|
||||||
return
|
return copied_entities
|
||||||
dest.Representation = ifcopenshell.util.element.copy_deep(
|
dest.Representation = ifcopenshell.util.element.copy_deep(
|
||||||
tool.Ifc.get(),
|
tool.Ifc.get(),
|
||||||
source.Representation,
|
source.Representation,
|
||||||
exclude=["IfcGeometricRepresentationContext"],
|
exclude=["IfcGeometricRepresentationContext"],
|
||||||
exclude_callback=exclude_callback,
|
exclude_callback=exclude_callback,
|
||||||
|
copied_entities=copied_entities,
|
||||||
)
|
)
|
||||||
elif dest.is_a("IfcTypeProduct"):
|
elif dest.is_a("IfcTypeProduct"):
|
||||||
if not source.RepresentationMaps:
|
if not source.RepresentationMaps:
|
||||||
return
|
return copied_entities
|
||||||
dest.RepresentationMaps = [
|
dest.RepresentationMaps = [
|
||||||
ifcopenshell.util.element.copy_deep(
|
ifcopenshell.util.element.copy_deep(
|
||||||
tool.Ifc.get(), m, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback
|
tool.Ifc.get(),
|
||||||
|
m,
|
||||||
|
exclude=["IfcGeometricRepresentationContext"],
|
||||||
|
exclude_callback=exclude_callback,
|
||||||
|
copied_entities=copied_entities,
|
||||||
)
|
)
|
||||||
for m in source.RepresentationMaps
|
for m in source.RepresentationMaps
|
||||||
]
|
]
|
||||||
|
return copied_entities
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def does_type_have_representations(cls, element: ifcopenshell.entity_instance) -> bool:
|
def does_type_have_representations(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||||
@@ -354,7 +364,6 @@ class Root(bonsai.core.tool.Root):
|
|||||||
tool.Aggregate.constrain_all_parts_to_aggregate(tool.Ifc.get_object(new_aggregate[0]))
|
tool.Aggregate.constrain_all_parts_to_aggregate(tool.Ifc.get_object(new_aggregate[0]))
|
||||||
tool.Blender.select_and_activate_single_object(bpy.context, tool.Ifc.get_object(new_aggregate[0]))
|
tool.Blender.select_and_activate_single_object(bpy.context, tool.Ifc.get_object(new_aggregate[0]))
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run_geometry_add_representation(
|
def run_geometry_add_representation(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -550,6 +550,10 @@ class Style(bonsai.core.tool.Style):
|
|||||||
def is_editing_styles(cls) -> bool:
|
def is_editing_styles(cls) -> bool:
|
||||||
return bpy.context.scene.BIMStylesProperties.is_editing
|
return bpy.context.scene.BIMStylesProperties.is_editing
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_editing_style(cls) -> bool:
|
||||||
|
return bpy.context.scene.BIMStylesProperties.is_editing_style
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def select_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None:
|
def select_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None:
|
||||||
for element in elements:
|
for element in elements:
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
Debugging Bonsai
|
||||||
|
================
|
||||||
|
|
||||||
|
This is a mini-guide to setting up an IDE and configuring it to debug Bonsai more
|
||||||
|
easily. It is currently specific to this writers own system (Ubuntu) but this
|
||||||
|
can be expanded by others.
|
||||||
|
|
||||||
|
1. **Install VSCodium**: This will be system specific. I used the available
|
||||||
|
snap package.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sudo snap install --classic codium
|
||||||
|
|
||||||
|
I chose VSCodium to avoid Microsoft telemetry and data harvesting, but
|
||||||
|
VSCode is going to be similar, and even a bit easier (i.e. steps 3 & 4).
|
||||||
|
|
||||||
|
2. **Activate Python language support**: Start VSCodium, open the Extensions, find the Python
|
||||||
|
language support and activate it.
|
||||||
|
|
||||||
|
3. **Install Blender Addon**: It seems VSCodium is not allowed to directly access
|
||||||
|
the Marketplace, and this addon (and the next) is not available in
|
||||||
|
VSCodium's equivalent. (Or I'm an idiot... entirely possible). This means a
|
||||||
|
few more steps are needed.
|
||||||
|
|
||||||
|
Download the Blender addon's ``.vsix`` file from marketplace (Under "Resources")
|
||||||
|
using your browser from `this <https://marketplace.visualstudio.com/items?itemName=JacquesLucke.blender-development>`_
|
||||||
|
page.
|
||||||
|
|
||||||
|
Install it with "**...**" -> "**Install from VSIX...**"
|
||||||
|
|
||||||
|
4. **Install ms-vscode.cpptools**: When trying to use the Blender addon I got
|
||||||
|
an error about a missing dependancy. I read somewhere that this addon was
|
||||||
|
not needed, but I can't currently find the reference. At least for me, it
|
||||||
|
needs to be added the same way as step 3, or the Blender addon fails to
|
||||||
|
start.
|
||||||
|
|
||||||
|
Download the Cpp Tools addon's ``.vsix`` file from marketplace (Under "Resources")
|
||||||
|
using your browser from `this <https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools>`_
|
||||||
|
page. You will need to select the correct download for your system OS and
|
||||||
|
architecture.
|
||||||
|
|
||||||
|
Install it with "**...**" -> "**Install from VSIX...**"
|
||||||
|
|
||||||
|
5. **Set the Blender config directory**: In **Settings** -> **Extensions** -> **Blender** -> "**Environment Variables**"
|
||||||
|
edit the json file to set the ``BLENDER_USER_RESOURCE`` to the config folder
|
||||||
|
you installed Bonsai to. For me:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"blender.environmentVariables": {
|
||||||
|
"BLENDER_USER_RESOURCES": "/home/steve/.config/blender/4.2bonsai"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
You `must` change that path to the correct value for your system.
|
||||||
|
|
||||||
|
6. **Unset the Just My Code flag**: In **Settings** -> **Extensions** -> **Blender** -> "**Just My Code**"
|
||||||
|
by unchecking the box.
|
||||||
|
|
||||||
|
Again, this was advice found searching around in a github issue. If not
|
||||||
|
done, the breakpoints do not work for me.
|
||||||
|
|
||||||
|
7. **Open the folder in VSCodium**: This is the same folder set in step 5.
|
||||||
|
|
||||||
|
``/home/steve/.config/blender/4.2bonsai``
|
||||||
|
|
||||||
|
This was a key point for me. Do `not` go deeper in the folder structure -
|
||||||
|
that just doesn't work. (Thanks @theoryshaw)
|
||||||
|
|
||||||
|
8. **Start Blender**: In VSCodium press ``Ctrl+Shift+P``, and search/select
|
||||||
|
**Blender: Start**.
|
||||||
|
|
||||||
|
9. **Set the Blender binary path**: The first time you try to start Blender the
|
||||||
|
addon will ask for the path of the binary. Navigate to your Blender binary
|
||||||
|
and select it. For me:
|
||||||
|
|
||||||
|
``/home/steve/Software/blender-git/build_linux/bin/blender``
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
Do `not` try to use a binary installed using snap - it will `not` work.
|
||||||
|
Either install a debian package of Blender, or build it from source, and
|
||||||
|
use that.
|
||||||
|
|
||||||
|
All being well, Blender should start and show the usual Bonsai UI.
|
||||||
|
|
||||||
|
10. **Ensure Blender is behaving**: Exercise the interface a bit to be sure
|
||||||
|
Bonsai is working normally. Add a demo project, add some walls, etc.
|
||||||
|
|
||||||
|
One issue I ran in to was opening the wrong folder in step 7. Due to the
|
||||||
|
way VSCodium works, I was getting two Bonsai plugins conflicting, and
|
||||||
|
failing in interesting ways. One symptom of this was the modal user
|
||||||
|
interface when adding walls was completely missing. Oh, and the debugger
|
||||||
|
completely failed to work.
|
||||||
|
|
||||||
|
If you are happy, quit Blender, and try setting some breakpoints in
|
||||||
|
VSCodium then run Blender again, and try to trigger them. I set one on
|
||||||
|
the ``bim.add_sheet`` operator at:
|
||||||
|
``extensions/.local/lib/python3.11/site-packages/bonsai/bim/module/drawing/operator.py:1456``
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
I made the mistake of setting the breakpoint on the ``_execute``
|
||||||
|
methods ``def`` line, which did not work. This probably helped confuse
|
||||||
|
me when trying to get the addon to work.
|
||||||
|
|
||||||
|
If you get to this point, congratulations! You will now be 1000% more effective
|
||||||
|
when troubleshooting issues, and able to make many more contributions, fixes
|
||||||
|
and patches.
|
||||||
@@ -15,3 +15,4 @@ This chapter covers how you can help contribute to Bonsai.
|
|||||||
translations
|
translations
|
||||||
undo_system
|
undo_system
|
||||||
writing_docs
|
writing_docs
|
||||||
|
debugging
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ and data-rich OpenBIM with Blender :)
|
|||||||
guides/development/index
|
guides/development/index
|
||||||
guides/authoring/other_addons
|
guides/authoring/other_addons
|
||||||
guides/troubleshooting
|
guides/troubleshooting
|
||||||
|
guides/debugging
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
|
|||||||
@@ -296,12 +296,12 @@ class IfcCsv:
|
|||||||
writer.writerow(self.headers)
|
writer.writerow(self.headers)
|
||||||
for row in self.results:
|
for row in self.results:
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
if any([s for s in self.summaries if s is not None]):
|
if self.has_summaries():
|
||||||
writer.writerow(self.summaries)
|
writer.writerow(self.summaries)
|
||||||
|
|
||||||
def export_ods(self, output, should_preserve_existing=False):
|
def export_ods(self, output, should_preserve_existing=False):
|
||||||
df = self.export_pd()
|
df = self.export_pd()
|
||||||
if self.summaries:
|
if self.has_summaries():
|
||||||
df.loc[df.shape[0]] = self.summaries
|
df.loc[df.shape[0]] = self.summaries
|
||||||
|
|
||||||
if os.path.exists(output) and should_preserve_existing:
|
if os.path.exists(output) and should_preserve_existing:
|
||||||
@@ -324,10 +324,9 @@ class IfcCsv:
|
|||||||
# If the DataFrame has fewer rows than the table, blank out the extra rows
|
# If the DataFrame has fewer rows than the table, blank out the extra rows
|
||||||
num_rows_table = len(first_table.getElementsByType(TableRow)) - 1 # Exclude header row
|
num_rows_table = len(first_table.getElementsByType(TableRow)) - 1 # Exclude header row
|
||||||
if len(df) < num_rows_table:
|
if len(df) < num_rows_table:
|
||||||
for i in range(len(df) + 1, num_rows_table + 1): # +1 to account for header
|
rows = first_table.getElementsByType(TableRow)
|
||||||
for cell in first_table.getElementsByType(TableRow)[i].getElementsByType(TableCell):
|
for i in reversed(range(len(df) + 1, num_rows_table + 1)): # +1 to account for header
|
||||||
for item in cell.childNodes:
|
first_table.removeChild(rows[i])
|
||||||
cell.removeChild(item)
|
|
||||||
|
|
||||||
ods_document.save(output)
|
ods_document.save(output)
|
||||||
else:
|
else:
|
||||||
@@ -362,9 +361,12 @@ class IfcCsv:
|
|||||||
row.addElement(new_cell)
|
row.addElement(new_cell)
|
||||||
return new_cell
|
return new_cell
|
||||||
|
|
||||||
|
def has_summaries(self):
|
||||||
|
return any([s for s in self.summaries if s is not None])
|
||||||
|
|
||||||
def export_xlsx(self, output, should_preserve_existing=False):
|
def export_xlsx(self, output, should_preserve_existing=False):
|
||||||
df = self.export_pd()
|
df = self.export_pd()
|
||||||
if self.summaries:
|
if self.has_summaries():
|
||||||
df.loc[df.shape[0]] = self.summaries
|
df.loc[df.shape[0]] = self.summaries
|
||||||
|
|
||||||
if os.path.exists(output):
|
if os.path.exists(output):
|
||||||
|
|||||||
@@ -130,7 +130,15 @@ class Usecase:
|
|||||||
continue
|
continue
|
||||||
elif obj.is_a("IfcFeatureElement"):
|
elif obj.is_a("IfcFeatureElement"):
|
||||||
# Feature elements affect the geometry of their parent, and
|
# Feature elements affect the geometry of their parent, and
|
||||||
# so logically should always move with the parent.
|
# so logically should always move with the parent. However,
|
||||||
|
# subchildren shouldn't move.
|
||||||
|
placement2 = obj.ObjectPlacement
|
||||||
|
for referenced_placement2 in placement2.ReferencedByPlacements:
|
||||||
|
matrix2 = ifcopenshell.util.placement.get_local_placement(referenced_placement2)
|
||||||
|
for obj2 in referenced_placement2.PlacesObject:
|
||||||
|
results.append(
|
||||||
|
{"product": obj2, "matrix": matrix2, "is_si": False, "should_transform_children": True}
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
results.append({"product": obj, "matrix": matrix, "is_si": False, "should_transform_children": True})
|
results.append({"product": obj, "matrix": matrix, "is_si": False, "should_transform_children": True})
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from .add_context_dependent_unit import add_context_dependent_unit
|
|||||||
from .add_conversion_based_unit import add_conversion_based_unit
|
from .add_conversion_based_unit import add_conversion_based_unit
|
||||||
from .add_monetary_unit import add_monetary_unit
|
from .add_monetary_unit import add_monetary_unit
|
||||||
from .add_si_unit import add_si_unit
|
from .add_si_unit import add_si_unit
|
||||||
|
from .add_derived_unit import add_derived_unit
|
||||||
from .assign_unit import assign_unit
|
from .assign_unit import assign_unit
|
||||||
from .edit_derived_unit import edit_derived_unit
|
from .edit_derived_unit import edit_derived_unit
|
||||||
from .edit_monetary_unit import edit_monetary_unit
|
from .edit_monetary_unit import edit_monetary_unit
|
||||||
@@ -42,6 +43,7 @@ __all__ = [
|
|||||||
"add_conversion_based_unit",
|
"add_conversion_based_unit",
|
||||||
"add_monetary_unit",
|
"add_monetary_unit",
|
||||||
"add_si_unit",
|
"add_si_unit",
|
||||||
|
"add_derived_unit",
|
||||||
"assign_unit",
|
"assign_unit",
|
||||||
"edit_derived_unit",
|
"edit_derived_unit",
|
||||||
"edit_monetary_unit",
|
"edit_monetary_unit",
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# IfcOpenShell - IFC toolkit and geometry engine
|
||||||
|
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# This file is part of IfcOpenShell.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import ifcopenshell.util.unit
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def add_derived_unit(
|
||||||
|
file: ifcopenshell.file, unit_type: str, userdefinedtype: str, attributes: dict[ifcopenshell.entity_instance, int]
|
||||||
|
) -> ifcopenshell.entity_instance:
|
||||||
|
"""Add a new Derive unit
|
||||||
|
|
||||||
|
The supported types are ANGULARVELOCITYUNIT, AREADENSITYUNIT, COMPOUNDPLANEANGLEUNIT,
|
||||||
|
DYNAMICVISCOSITYUNIT, HEATFLUXDENSITYUNIT, INTEGERCOUNTRATEUNIT, ISOTHERMALMOISTURECAPACITYUNIT,
|
||||||
|
KINEMATICVISCOSITYUNIT, LINEARVELOCITYUNIT, MASSDENSITYUNIT, MASSFLOWRATEUNIT, MOISTUREDIFFUSIVITYUNIT,
|
||||||
|
MOLECULARWEIGHTUNIT, SPECIFICHEATCAPACITYUNIT, THERMALADMITTANCEUNIT, THERMALCONDUCTANCEUNIT,
|
||||||
|
THERMALRESISTANCEUNIT, THERMALTRANSMITTANCEUNIT, VAPORPERMEABILITYUNIT, VOLUMETRICFLOWRATEUNIT,
|
||||||
|
ROTATIONALFREQUENCYUNIT, TORQUEUNIT, MOMENTOFINERTIAUNIT, LINEARMOMENTUNIT, LINEARFORCEUNIT,
|
||||||
|
PLANARFORCEUNIT, MODULUSOFELASTICITYUNIT, SHEARMODULUSUNIT, LINEARSTIFFNESSUNIT, ROTATIONALSTIFFNESSUNIT,
|
||||||
|
MODULUSOFSUBGRADEREACTIONUNIT, ACCELERATIONUNIT, CURVATUREUNIT, HEATINGVALUEUNIT, IONCONCENTRATIONUNIT,
|
||||||
|
LUMINOUSINTENSITYDISTRIBUTIONUNIT, MASSPERLENGTHUNIT, MODULUSOFLINEARSUBGRADEREACTIONUNIT,
|
||||||
|
MODULUSOFROTATIONALSUBGRADEREACTIONUNIT, PHUNIT, ROTATIONALMASSUNIT, SECTIONAREAINTEGRALUNIT,
|
||||||
|
SECTIONMODULUSUNIT, SOUNDPOWERLEVELUNIT, SOUNDPOWERUNIT, SOUNDPRESSURELEVELUNIT, SOUNDPRESSUREUNIT,
|
||||||
|
TEMPERATUREGRADIENTUNIT, TEMPERATURERATEOFCHANGEUNIT, THERMALEXPANSIONCOEFFICIENTUNIT, WARPINGCONSTANTUNIT,
|
||||||
|
WARPINGMOMENTUNIT, USERDEFINED.
|
||||||
|
|
||||||
|
In case of choosing USERDEFINED, the UserDefinedType parameter needs to be provided
|
||||||
|
|
||||||
|
:param unit_type: A type of unit chosen from the list above. For
|
||||||
|
example, choosing THERMALCONDUCTANCEUNIT will give you a Thermal conductance.
|
||||||
|
:type unit_type: str
|
||||||
|
:param userdefinedtype: The user defined type in case of choosing USERDEFINED, or None for no
|
||||||
|
user defined type.
|
||||||
|
:type userdefinedtype: str or None
|
||||||
|
:param attributes: a dictionary of attribute names and values.
|
||||||
|
:type attributes: dict
|
||||||
|
:return: The newly created IfcDerivedUnit
|
||||||
|
:rtype: ifcopenshell.entity_instance
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# Linear velocity in m/s
|
||||||
|
length = ifcopenshell.api.unit.add_si_unit(model, unit_type="LENGTHUNIT")
|
||||||
|
#2=IfcSIUnit(*,.LENGTHUNIT.,$,.METRE.)
|
||||||
|
|
||||||
|
time = ifcopenshell.api.unit.add_si_unit(model, unit_type="TIMEUNIT")
|
||||||
|
#4=IfcSIUnit(*,.TIMEUNIT.,$,.SECOND.)
|
||||||
|
|
||||||
|
linear_velocity = ifcopenshell.api.unit.add_derived_unit(model, 'LINEARVELOCITY', None, {length : 1, time : -1})
|
||||||
|
#10=IfcDerivedUnitElement(#2, 1)
|
||||||
|
#11=IfcDerivedUnitElement(#4, -1)
|
||||||
|
#12=IfcDerivedUnit((#10,#11),.LINEARVELOCITY.,$)
|
||||||
|
|
||||||
|
"""
|
||||||
|
settings = {"unit_type": unit_type, "attributes": attributes}
|
||||||
|
|
||||||
|
derive_unit_elements = []
|
||||||
|
|
||||||
|
for named_unit in settings["attributes"]:
|
||||||
|
derive_unit_elements.append(
|
||||||
|
file.create_entity("IfcDerivedUnitElement", Unit=named_unit, Exponent=settings["attributes"][named_unit])
|
||||||
|
)
|
||||||
|
|
||||||
|
return file.create_entity(
|
||||||
|
"IfcDerivedUnit", Elements=derive_unit_elements, UnitType=settings["unit_type"], UserDefinedType=userdefinedtype
|
||||||
|
)
|
||||||
@@ -74,7 +74,7 @@ SETTING = Literal[
|
|||||||
"no-wire-intersection-check",
|
"no-wire-intersection-check",
|
||||||
"no-wire-intersection-tolerance",
|
"no-wire-intersection-tolerance",
|
||||||
"precision-factor",
|
"precision-factor",
|
||||||
"debug",
|
"debug-boolean",
|
||||||
"boolean-attempt-2d",
|
"boolean-attempt-2d",
|
||||||
"weld-vertices",
|
"weld-vertices",
|
||||||
"use-world-coords",
|
"use-world-coords",
|
||||||
|
|||||||
@@ -763,37 +763,33 @@ class FacetTransformer(lark.Transformer):
|
|||||||
else:
|
else:
|
||||||
self.base_elements = elements.copy()
|
self.base_elements = elements.copy()
|
||||||
self.elements = set()
|
self.elements = set()
|
||||||
|
self.has_additive_facet_in_current_list = False
|
||||||
self.container_trees = {}
|
self.container_trees = {}
|
||||||
|
|
||||||
|
def add_default_elements(self):
|
||||||
|
if self.has_additive_facet_in_current_list:
|
||||||
|
return
|
||||||
|
self.has_additive_facet_in_current_list = True
|
||||||
|
if self.base_elements:
|
||||||
|
self.elements.update(self.base_elements)
|
||||||
|
else:
|
||||||
|
self.elements.update(self.file.by_type("IfcProduct"))
|
||||||
|
self.elements.update(self.file.by_type("IfcTypeProduct"))
|
||||||
|
|
||||||
def get_results(self) -> set[ifcopenshell.entity_instance]:
|
def get_results(self) -> set[ifcopenshell.entity_instance]:
|
||||||
results: set[ifcopenshell.entity_instance] = set()
|
results: set[ifcopenshell.entity_instance] = set()
|
||||||
for r in self.results:
|
for r in self.results:
|
||||||
results |= r
|
results |= r
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def transform(self, tree: lark.ParseTree):
|
|
||||||
def has_elements_token(tree: lark.tree.ParseTree) -> bool:
|
|
||||||
for child in tree.iter_subtrees_topdown():
|
|
||||||
data = child.data
|
|
||||||
# Tokens that add elements to filter.
|
|
||||||
if data in ("entity", "instance"):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Only elements tokens can add elements to the filter.
|
|
||||||
# Fallback to default elements if they are not provided.
|
|
||||||
if self.base_elements is None and not has_elements_token(tree):
|
|
||||||
self.elements.update(self.file.by_type("IfcProduct"))
|
|
||||||
self.elements.update(self.file.by_type("IfcTypeProduct"))
|
|
||||||
|
|
||||||
return super().transform(tree)
|
|
||||||
|
|
||||||
def facet_list(self, args):
|
def facet_list(self, args):
|
||||||
if self.elements:
|
if self.elements:
|
||||||
self.results.append(self.elements)
|
self.results.append(self.elements)
|
||||||
self.elements = set()
|
self.elements = set()
|
||||||
|
self.has_additive_facet_in_current_list = False
|
||||||
|
|
||||||
def instance(self, args):
|
def instance(self, args):
|
||||||
|
self.has_additive_facet_in_current_list = True
|
||||||
if self.base_elements is None:
|
if self.base_elements is None:
|
||||||
if args[0].data == "globalid":
|
if args[0].data == "globalid":
|
||||||
try:
|
try:
|
||||||
@@ -816,6 +812,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def entity(self, args):
|
def entity(self, args):
|
||||||
|
self.has_additive_facet_in_current_list = True
|
||||||
if self.base_elements is None:
|
if self.base_elements is None:
|
||||||
if args[0].data == "ifc_class":
|
if args[0].data == "ifc_class":
|
||||||
try:
|
try:
|
||||||
@@ -844,6 +841,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
element_value = getattr(element, name, None)
|
element_value = getattr(element, name, None)
|
||||||
return self.compare(element_value, comparison, value)
|
return self.compare(element_value, comparison, value)
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def type(self, args):
|
def type(self, args):
|
||||||
@@ -853,6 +851,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
element_value = getattr(ifcopenshell.util.element.get_type(element), "Name", None)
|
element_value = getattr(ifcopenshell.util.element.get_type(element), "Name", None)
|
||||||
return self.compare(element_value, comparison, value)
|
return self.compare(element_value, comparison, value)
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def material(self, args):
|
def material(self, args):
|
||||||
@@ -870,6 +869,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
return result if comparison == "=" else not result
|
return result if comparison == "=" else not result
|
||||||
return self.compare(None, comparison, value)
|
return self.compare(None, comparison, value)
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def property(self, args):
|
def property(self, args):
|
||||||
@@ -899,6 +899,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
return self.compare(element_value, comparison, value)
|
return self.compare(element_value, comparison, value)
|
||||||
return self.compare(None, comparison, value)
|
return self.compare(None, comparison, value)
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def classification(self, args):
|
def classification(self, args):
|
||||||
@@ -918,6 +919,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
return result if comparison == "=" else not result
|
return result if comparison == "=" else not result
|
||||||
return self.compare(None, comparison, value)
|
return self.compare(None, comparison, value)
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def location(self, args):
|
def location(self, args):
|
||||||
@@ -936,6 +938,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
return result if comparison == "=" else not result
|
return result if comparison == "=" else not result
|
||||||
return self.compare(None, comparison, value)
|
return self.compare(None, comparison, value)
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def group(self, args):
|
def group(self, args):
|
||||||
@@ -949,6 +952,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
result = True
|
result = True
|
||||||
return result if comparison == "=" else not result
|
return result if comparison == "=" else not result
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def parent(self, args):
|
def parent(self, args):
|
||||||
@@ -989,6 +993,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
for parent in parents:
|
for parent in parents:
|
||||||
children |= set(ifcopenshell.util.element.get_decomposition(parent))
|
children |= set(ifcopenshell.util.element.get_decomposition(parent))
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
if comparison == "=":
|
if comparison == "=":
|
||||||
self.elements = self.elements & children
|
self.elements = self.elements & children
|
||||||
else:
|
else:
|
||||||
@@ -1000,6 +1005,7 @@ class FacetTransformer(lark.Transformer):
|
|||||||
def filter_function(element: ifcopenshell.entity_instance) -> bool:
|
def filter_function(element: ifcopenshell.entity_instance) -> bool:
|
||||||
return self.compare(get_element_value(element, keys), comparison, value)
|
return self.compare(get_element_value(element, keys), comparison, value)
|
||||||
|
|
||||||
|
self.add_default_elements()
|
||||||
self.elements = set(filter(filter_function, self.elements))
|
self.elements = set(filter(filter_function, self.elements))
|
||||||
|
|
||||||
def get_container_tree(self, container: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
def get_container_tree(self, container: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import ifcopenshell.util.unit
|
|||||||
from math import cos, sin, pi, tan, radians, degrees, atan, sqrt
|
from math import cos, sin, pi, tan, radians, degrees, atan, sqrt
|
||||||
from typing import Union, Optional, Literal, Any, Sequence, TYPE_CHECKING
|
from typing import Union, Optional, Literal, Any, Sequence, TYPE_CHECKING
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from mathutils import Vector
|
|
||||||
|
|
||||||
PRECISION = 1.0e-5
|
PRECISION = 1.0e-5
|
||||||
|
|
||||||
|
|||||||
@@ -289,15 +289,11 @@ unit_symbols = {
|
|||||||
"SECOND": "s",
|
"SECOND": "s",
|
||||||
"SQUARE_METRE": "m2",
|
"SQUARE_METRE": "m2",
|
||||||
"METRE": "m",
|
"METRE": "m",
|
||||||
# non si units
|
"NEWTON": "N",
|
||||||
"cubic inch": "in3",
|
"PASCAL": "Pa",
|
||||||
"cubic foot": "ft3",
|
|
||||||
"cubic yard": "yd3",
|
|
||||||
"square inch": "in2",
|
|
||||||
"square foot": "ft2",
|
|
||||||
"square yard": "yd2",
|
|
||||||
"square mile": "mi2",
|
|
||||||
# conversion based units
|
# conversion based units
|
||||||
|
"pound-force": "lbf",
|
||||||
|
"pound-force per square inch": "psi",
|
||||||
"thou": "th",
|
"thou": "th",
|
||||||
"inch": "in",
|
"inch": "in",
|
||||||
"foot": "ft",
|
"foot": "ft",
|
||||||
|
|||||||
@@ -545,11 +545,12 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
|
|||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
self.file.by_id(previous_placement_id)
|
self.file.by_id(previous_placement_id)
|
||||||
|
|
||||||
def test_changing_placements_always_affecting_child_features_as_a_special_case(self):
|
def test_changing_placements_always_affecting_child_features_but_not_subchildren_as_a_special_case(self):
|
||||||
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||||
ifcopenshell.api.unit.assign_unit(self.file)
|
ifcopenshell.api.unit.assign_unit(self.file)
|
||||||
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
subelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
|
subelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
|
||||||
|
subsubelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
|
||||||
|
|
||||||
matrix = numpy.eye(4)
|
matrix = numpy.eye(4)
|
||||||
matrix[:3, 3] = (1, 1, 1)
|
matrix[:3, 3] = (1, 1, 1)
|
||||||
@@ -557,16 +558,23 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
|
|||||||
submatrix = numpy.eye(4)
|
submatrix = numpy.eye(4)
|
||||||
submatrix[:3, 3] = (1, 2, 3)
|
submatrix[:3, 3] = (1, 2, 3)
|
||||||
|
|
||||||
|
subsubmatrix = numpy.eye(4)
|
||||||
|
subsubmatrix[:3, 3] = (7, 8, 9)
|
||||||
|
|
||||||
shifted_submatrix = numpy.eye(4)
|
shifted_submatrix = numpy.eye(4)
|
||||||
shifted_submatrix[:3, 3] = (1, 3, 5)
|
shifted_submatrix[:3, 3] = (1, 3, 5)
|
||||||
|
|
||||||
ifcopenshell.api.void.add_opening(self.file, opening=subelement, element=element)
|
ifcopenshell.api.void.add_opening(self.file, opening=subelement, element=element)
|
||||||
|
ifcopenshell.api.void.add_filling(self.file, opening=subelement, element=subsubelement)
|
||||||
previous_placement_id = ifcopenshell.api.geometry.edit_object_placement(
|
previous_placement_id = ifcopenshell.api.geometry.edit_object_placement(
|
||||||
self.file, product=element, matrix=matrix.copy(), is_si=False
|
self.file, product=element, matrix=matrix.copy(), is_si=False
|
||||||
).id()
|
).id()
|
||||||
ifcopenshell.api.geometry.edit_object_placement(
|
ifcopenshell.api.geometry.edit_object_placement(
|
||||||
self.file, product=subelement, matrix=submatrix.copy(), is_si=False
|
self.file, product=subelement, matrix=submatrix.copy(), is_si=False
|
||||||
)
|
)
|
||||||
|
ifcopenshell.api.geometry.edit_object_placement(
|
||||||
|
self.file, product=subsubelement, matrix=subsubmatrix.copy(), is_si=False
|
||||||
|
)
|
||||||
ifcopenshell.api.geometry.edit_object_placement(
|
ifcopenshell.api.geometry.edit_object_placement(
|
||||||
self.file,
|
self.file,
|
||||||
product=element,
|
product=element,
|
||||||
@@ -578,7 +586,11 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
|
|||||||
assert numpy.array_equal(
|
assert numpy.array_equal(
|
||||||
ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), shifted_submatrix
|
ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), shifted_submatrix
|
||||||
)
|
)
|
||||||
|
assert numpy.array_equal(
|
||||||
|
ifcopenshell.util.placement.get_local_placement(subsubelement.ObjectPlacement), subsubmatrix
|
||||||
|
)
|
||||||
assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement
|
assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement
|
||||||
|
assert subsubelement.ObjectPlacement.PlacementRelTo == subelement.ObjectPlacement
|
||||||
# old placement should be removed to avoid orphaned entities
|
# old placement should be removed to avoid orphaned entities
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
self.file.by_id(previous_placement_id)
|
self.file.by_id(previous_placement_id)
|
||||||
|
|||||||
@@ -95,30 +95,16 @@ def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
|
|||||||
else:
|
else:
|
||||||
recipe = importlib.import_module(f"ifcpatch.recipes.{args['recipe']}")
|
recipe = importlib.import_module(f"ifcpatch.recipes.{args['recipe']}")
|
||||||
|
|
||||||
# Ensure file or input is provided.
|
|
||||||
input_argument = get_patch_input_argument_use(args["recipe"])
|
|
||||||
if input_argument == "REQUIRED":
|
|
||||||
if not args.get("input"):
|
|
||||||
raise ValueError(f"Recipe {args['recipe']} is requiring 'input' argument to be provided.")
|
|
||||||
elif "file" not in args: # SUPPORTED, IGNORED.
|
|
||||||
raise ValueError(f"Recipe {args['recipe']} is requiring 'file' argument to be provided.")
|
|
||||||
|
|
||||||
arguments = args.get("arguments", None) or []
|
arguments = args.get("arguments", None) or []
|
||||||
if recipe.Patcher.__init__.__doc__ is not None:
|
if recipe.Patcher.__init__.__doc__ is not None:
|
||||||
patcher = recipe.Patcher(args.get("input"), args.get("file"), logger, *arguments)
|
patcher = recipe.Patcher(args.get("file"), logger, *arguments)
|
||||||
else:
|
else:
|
||||||
patcher = recipe.Patcher(args.get("input"), args.get("file"), logger, arguments)
|
patcher = recipe.Patcher(args.get("file"), logger, arguments)
|
||||||
patcher.patch()
|
patcher.patch()
|
||||||
output = getattr(patcher, "file_patched", patcher.file)
|
output = getattr(patcher, "file_patched", patcher.file)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def get_patch_input_argument_use(recipe: str) -> Literal["REQUIRED", "SUPPORTED", "IGNORED"]:
|
|
||||||
# try out of tree and subpackage imports
|
|
||||||
recipe_module = sys.modules.get(f"ifcpatch.recipes.{recipe}") or sys.modules.get(recipe)
|
|
||||||
return getattr(recipe_module.Patcher, "input_argument", "IGNORED")
|
|
||||||
|
|
||||||
|
|
||||||
def write(output: Union[ifcopenshell.file, str], filepath: str) -> None:
|
def write(output: Union[ifcopenshell.file, str], filepath: str) -> None:
|
||||||
"""Write the output of an IFC patch to a file
|
"""Write the output of an IFC patch to a file
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ LengthUnit = typing.Literal[
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src: str,
|
|
||||||
file: ifcopenshell.file,
|
file: ifcopenshell.file,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
unit: LengthUnit = "METER",
|
unit: LengthUnit = "METER",
|
||||||
@@ -76,7 +75,6 @@ class Patcher:
|
|||||||
# Convert to feet
|
# Convert to feet
|
||||||
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
|
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.unit = unit
|
self.unit = unit
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import ifcopenshell.guid
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Convert nesting relationships to aggregate relationships
|
"""Convert nesting relationships to aggregate relationships
|
||||||
|
|
||||||
Some software like Revit won't load nested children elements because
|
Some software like Revit won't load nested children elements because
|
||||||
@@ -39,7 +39,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertNestToAggregate", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertNestToAggregate", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from typing import Union
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, property_name: str, quantity_name: str):
|
def __init__(self, file: ifcopenshell.file, logger: Logger, property_name: str, quantity_name: str):
|
||||||
"""Converts a property to a standardised quantity
|
"""Converts a property to a standardised quantity
|
||||||
|
|
||||||
IFC can store arbitrary key value metadata associated with a elements
|
IFC can store arbitrary key value metadata associated with a elements
|
||||||
@@ -59,7 +59,6 @@ class Patcher:
|
|||||||
# "NetSideArea", if that standardised quantity exists.
|
# "NetSideArea", if that standardised quantity exists.
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertPropertiesToQuantities", "arguments": ["Area", "NetSideArea"]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertPropertiesToQuantities", "arguments": ["Area", "NetSideArea"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.source_property_name = property_name
|
self.source_property_name = property_name
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import ifcopenshell.util.element
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Downgrade indexed polycurves to simple polylines
|
"""Downgrade indexed polycurves to simple polylines
|
||||||
|
|
||||||
Low quality IFC viewers like Navisworks do not support various IFC4
|
Low quality IFC viewers like Navisworks do not support various IFC4
|
||||||
@@ -39,7 +39,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "DowngradeIndexedPolyCurve", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "DowngradeIndexedPolyCurve", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from logging import Logger
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, query: str = "IfcWall"):
|
def __init__(self, file: ifcopenshell.file, logger: Logger, query: str = "IfcWall"):
|
||||||
"""Extract certain elements into a new model
|
"""Extract certain elements into a new model
|
||||||
|
|
||||||
Extract a subset of elements from an existing IFC data set and save it
|
Extract a subset of elements from an existing IFC data set and save it
|
||||||
@@ -48,7 +48,6 @@ class Patcher:
|
|||||||
# Extract all walls and slabs
|
# Extract all walls and slabs
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.query = query
|
self.query = query
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ except:
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src,
|
|
||||||
file,
|
file,
|
||||||
logger,
|
logger,
|
||||||
):
|
):
|
||||||
@@ -50,7 +49,6 @@ class Patcher:
|
|||||||
result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"})
|
result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"})
|
||||||
ifcpatch.write(result, "output.sqlite")
|
ifcpatch.write(result, "output.sqlite")
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import ifcopenshell.util.element
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Fix missing door swings in Revit when viewing ArchiCAD IFCs
|
"""Fix missing door swings in Revit when viewing ArchiCAD IFCs
|
||||||
|
|
||||||
ArchiCAD has the ability to store 2D data with objects like doors for
|
ArchiCAD has the ability to store 2D data with objects like doors for
|
||||||
@@ -56,7 +56,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixArchiCADToRevitDoorSwings", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixArchiCADToRevitDoorSwings", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ import ifcopenshell
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
input_argument = "REQUIRED"
|
def __init__(self, file: None, logger: logging.Logger, filepath: str):
|
||||||
|
|
||||||
def __init__(self, src: str, file: None, logger: logging.Logger):
|
|
||||||
"""Allow ArchiCAD IFC spaces to open as Revit rooms
|
"""Allow ArchiCAD IFC spaces to open as Revit rooms
|
||||||
|
|
||||||
The underlying problem is that Revit does not bring in IFC spaces as
|
The underlying problem is that Revit does not bring in IFC spaces as
|
||||||
@@ -51,16 +49,21 @@ class Patcher:
|
|||||||
requires you to run it using Blender, as the geometric modification
|
requires you to run it using Blender, as the geometric modification
|
||||||
uses the Blender geometry engine.
|
uses the Blender geometry engine.
|
||||||
|
|
||||||
`input` argument is required for this recipe, `file` argument is ignored.
|
`filepath` argument is required for this recipe, `file` argument is
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
:param filepath: The filepath of the IFC model. This is required to
|
||||||
|
load into Bonsai.
|
||||||
|
:filter_glob filepath: *.ifc;*.ifczip;*.ifcxml
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
ifcpatch.execute({"input": "input.ifc", "recipe": "FixArchiCADToRevitSpaces", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "recipe": "FixArchiCADToRevitSpaces", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
self.filepath = filepath
|
||||||
|
|
||||||
def patch(self) -> None:
|
def patch(self) -> None:
|
||||||
import bpy
|
import bpy
|
||||||
@@ -74,7 +77,7 @@ class Patcher:
|
|||||||
bpy.data.batch_remove(bpy.data.objects)
|
bpy.data.batch_remove(bpy.data.objects)
|
||||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||||
|
|
||||||
bpy.ops.bim.load_project(filepath=self.src)
|
bpy.ops.bim.load_project(filepath=self.filepath)
|
||||||
|
|
||||||
def recalculate_origin(wall: bpy.types.Object) -> None:
|
def recalculate_origin(wall: bpy.types.Object) -> None:
|
||||||
new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
|
new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import ifcopenshell.util.element
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Reassigns occurrence classifications to types
|
"""Reassigns occurrence classifications to types
|
||||||
|
|
||||||
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/691)
|
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/691)
|
||||||
@@ -35,7 +35,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixRevitClassificationCodeTypes"})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixRevitClassificationCodeTypes"})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ import logging
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
input_argument = "REQUIRED"
|
def __init__(self, file: None, logger: logging.Logger, filepath: str, is_solid: bool = True):
|
||||||
|
|
||||||
def __init__(self, src: str, file: None, logger: logging.Logger, is_solid: bool = True):
|
|
||||||
"""Fix missing or spot-coordinate bugged TINs loading in Revit
|
"""Fix missing or spot-coordinate bugged TINs loading in Revit
|
||||||
|
|
||||||
TINs exported from 12D or Civil 3D may contain dense or highly obtuse
|
TINs exported from 12D or Civil 3D may contain dense or highly obtuse
|
||||||
@@ -62,7 +60,12 @@ class Patcher:
|
|||||||
from civil software. It also requires you to run it using Blender, as
|
from civil software. It also requires you to run it using Blender, as
|
||||||
the geometric modification uses the Blender geometry engine.
|
the geometric modification uses the Blender geometry engine.
|
||||||
|
|
||||||
`input` argument is required for this recipe, `file` argument is ignored.
|
`filepath` argument is required for this recipe, `file` argument is
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
:param filepath: The filepath of the IFC model. This is required to
|
||||||
|
load into Bonsai.
|
||||||
|
:filter_glob filepath: *.ifc;*.ifczip;*.ifcxml
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -70,8 +73,8 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "recipe": "FixRevitTINs", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "recipe": "FixRevitTINs", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
|
self.filepath = filepath
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.is_solid = is_solid
|
self.is_solid = is_solid
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ class Patcher:
|
|||||||
from math import degrees
|
from math import degrees
|
||||||
|
|
||||||
bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True
|
bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True
|
||||||
bpy.ops.bim.load_project(filepath=self.src)
|
bpy.ops.bim.load_project(filepath=self.filepath)
|
||||||
|
|
||||||
old_history_size = tool.Ifc.get().history_size
|
old_history_size = tool.Ifc.get().history_size
|
||||||
old_undo_steps = bpy.context.preferences.edit.undo_steps
|
old_undo_steps = bpy.context.preferences.edit.undo_steps
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ except:
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src,
|
|
||||||
file,
|
file,
|
||||||
logger,
|
logger,
|
||||||
sql_type: SQLTypes = "SQLite",
|
sql_type: SQLTypes = "SQLite",
|
||||||
@@ -98,7 +97,6 @@ class Patcher:
|
|||||||
{"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]}
|
{"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]}
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.sql_type = sql_type.lower()
|
self.sql_type = sql_type.lower()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from logging import Logger
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, attribute: str = "Tag"):
|
def __init__(self, file: ifcopenshell.file, logger: Logger, attribute: str = "Tag"):
|
||||||
"""Merge duplicate element types via the Tag or another attribute
|
"""Merge duplicate element types via the Tag or another attribute
|
||||||
|
|
||||||
Revit is notorious for creating many duplicate element types. Element
|
Revit is notorious for creating many duplicate element types. Element
|
||||||
@@ -60,7 +60,6 @@ class Patcher:
|
|||||||
# Explicitly say we want to merge based on the Name attribute
|
# Explicitly say we want to merge based on the Name attribute
|
||||||
ifcpatch.execute({"file": model, "recipe": "MergeDuplicateTypes", "arguments": ["Name"]})
|
ifcpatch.execute({"file": model, "recipe": "MergeDuplicateTypes", "arguments": ["Name"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.attribute = attribute
|
self.attribute = attribute
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ from logging import Logger
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(self, file: ifcopenshell.file, logger: Logger, filepaths: list[Union[str, ifcopenshell.file]]):
|
||||||
self, src: str, file: ifcopenshell.file, logger: Logger, filepaths: list[Union[str, ifcopenshell.file]]
|
|
||||||
):
|
|
||||||
"""Merge two or more IFC models into one
|
"""Merge two or more IFC models into one
|
||||||
|
|
||||||
Note that other than combining the two (or more) IfcProject elements into
|
Note that other than combining the two (or more) IfcProject elements into
|
||||||
@@ -50,7 +48,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "MergeProjects", "arguments": ["/path/to/model2.ifc"]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "MergeProjects", "arguments": ["/path/to/model2.ifc"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.filepaths = filepaths
|
self.filepaths = filepaths
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from logging import Logger
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src: str,
|
|
||||||
file: ifcopenshell.file,
|
file: ifcopenshell.file,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
|
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
|
||||||
@@ -45,7 +44,6 @@ class Patcher:
|
|||||||
# Upgrade an IFC2X3 model to IFC4
|
# Upgrade an IFC2X3 model to IFC4
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import typing
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src,
|
|
||||||
file,
|
file,
|
||||||
logger,
|
logger,
|
||||||
x: typing.Union[str, float] = "0",
|
x: typing.Union[str, float] = "0",
|
||||||
@@ -89,7 +88,6 @@ class Patcher:
|
|||||||
# Some crazy 3D rotation and offset
|
# Some crazy 3D rotation and offset
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetObjectPlacements", "arguments": [12.5,5,2,False,90,90,45]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetObjectPlacements", "arguments": [12.5,5,2,False,90,90,45]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.x = float(x)
|
self.x = float(x)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import typing
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger, z: typing.Union[str, float] = "0"):
|
def __init__(self, file, logger, z: typing.Union[str, float] = "0"):
|
||||||
"""Offset building storeys by a particular Z value
|
"""Offset building storeys by a particular Z value
|
||||||
|
|
||||||
All objects placed relative to the storeys will also be shifted.
|
All objects placed relative to the storeys will also be shifted.
|
||||||
@@ -35,7 +35,6 @@ class Patcher:
|
|||||||
# Shift all storeys up by 42 units
|
# Shift all storeys up by 42 units
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetStoreyElevations", "arguments": [42]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetStoreyElevations", "arguments": [42]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.z = float(z)
|
self.z = float(z)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import ifcopenshell.util.element
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Optimise the filesize of an IFC model
|
"""Optimise the filesize of an IFC model
|
||||||
|
|
||||||
It is possible to non-losslessly optimise the filesize of an IFC model.
|
It is possible to non-losslessly optimise the filesize of an IFC model.
|
||||||
@@ -46,7 +46,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Optimise", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Optimise", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
|
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from logging import Logger
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger):
|
def __init__(self, file: ifcopenshell.file, logger: Logger):
|
||||||
"""Purge IFC properties, relationships, and other data
|
"""Purge IFC properties, relationships, and other data
|
||||||
|
|
||||||
In some rare cases (i.e. "resetting" a model or for security purposes)
|
In some rare cases (i.e. "resetting" a model or for security purposes)
|
||||||
@@ -51,7 +51,6 @@ class Patcher:
|
|||||||
# Watch the world burn
|
# Watch the world burn
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "PurgeData", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "PurgeData", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import ifcopenshell.util.element
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: logging.Logger):
|
def __init__(self, file: ifcopenshell.file, logger: logging.Logger):
|
||||||
"""Optimise the filesize of an IFC model by reusing non-rooted elements
|
"""Optimise the filesize of an IFC model by reusing non-rooted elements
|
||||||
|
|
||||||
It is possible to non-losslessly optimise the filesize of an IFC model.
|
It is possible to non-losslessly optimise the filesize of an IFC model.
|
||||||
@@ -46,7 +46,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RecycleNonRootedElements", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RecycleNonRootedElements", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from logging import Logger
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, only_duplicates=False):
|
def __init__(self, file: ifcopenshell.file, logger: Logger, only_duplicates: bool = False):
|
||||||
"""Regenerate GlobalIds in an IFC model
|
"""Regenerate GlobalIds in an IFC model
|
||||||
|
|
||||||
All root elements in an IFC model must be identified by a unique Global
|
All root elements in an IFC model must be identified by a unique Global
|
||||||
@@ -48,7 +48,6 @@ class Patcher:
|
|||||||
# Regenerate only duplicate GlobalIds
|
# Regenerate only duplicate GlobalIds
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RegenerateGlobalIds", "arguments": [True]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RegenerateGlobalIds", "arguments": [True]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.only_duplicates = only_duplicates
|
self.only_duplicates = only_duplicates
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Removes the built-in Revit Uniformat classification.
|
"""Removes the built-in Revit Uniformat classification.
|
||||||
|
|
||||||
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/486)
|
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/486)
|
||||||
@@ -31,7 +31,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveRevitUniformatClassification"})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveRevitUniformatClassification"})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Removes any 3D geometry associated with a site or multiple sites
|
"""Removes any 3D geometry associated with a site or multiple sites
|
||||||
|
|
||||||
If no sites or no site geometry is present, nothing happens.
|
If no sites or no site geometry is present, nothing happens.
|
||||||
@@ -29,7 +29,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveSiteRepresentation", "arguments": []})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveSiteRepresentation", "arguments": []})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ from typing import Literal, Optional, Union
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src: str,
|
|
||||||
file: ifcopenshell.file,
|
file: ifcopenshell.file,
|
||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
mode: Literal[
|
mode: Literal[
|
||||||
@@ -99,7 +98,6 @@ class Patcher:
|
|||||||
# Reset all coordinates with an ordinate larger than 1000 by -500,-200,0
|
# Reset all coordinates with an ordinate larger than 1000 by -500,-200,0
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [False, 1000, -500,-200,0]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [False, 1000, -500,-200,0]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.mode = mode.lower()
|
self.mode = mode.lower()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger, ifc_class="IfcSite"):
|
def __init__(self, file, logger, ifc_class="IfcSite"):
|
||||||
"""Resets the location of a spatial element to 0,0,0
|
"""Resets the location of a spatial element to 0,0,0
|
||||||
|
|
||||||
Another more specialised patch to fix incorrect coordinate usage is to
|
Another more specialised patch to fix incorrect coordinate usage is to
|
||||||
@@ -35,7 +35,6 @@ class Patcher:
|
|||||||
# All IfcSites will shift back to 0,0,0.
|
# All IfcSites will shift back to 0,0,0.
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite"]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.ifc_class = ifc_class
|
self.ifc_class = ifc_class
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import typing
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src,
|
|
||||||
file,
|
file,
|
||||||
logger,
|
logger,
|
||||||
name: str = "EPSG:1234",
|
name: str = "EPSG:1234",
|
||||||
@@ -59,7 +58,6 @@ class Patcher:
|
|||||||
# Set the current origin 0,0,0 to correlate to map coordinates 1000,1000,0 and a grid north of 15.
|
# Set the current origin 0,0,0 to correlate to map coordinates 1000,1000,0 and a grid north of 15.
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetFalseOrigin", "arguments": ["EPSG:1234", 0, 0, 0, 1000, 1000, 0, 15, 0]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetFalseOrigin", "arguments": ["EPSG:1234", 0, 0, 0, 1000, 1000, 0, 15, 0]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -73,7 +71,7 @@ class Patcher:
|
|||||||
self.rotate_angle = float(rotate_angle)
|
self.rotate_angle = float(rotate_angle)
|
||||||
|
|
||||||
def patch(self):
|
def patch(self):
|
||||||
SetWorldCoordinateSystem.Patcher(self.src, self.file, self.logger, x=0, y=0, z=0, ax=0, ay=0, az=0).patch()
|
SetWorldCoordinateSystem.Patcher(self.file, self.logger, x=0, y=0, z=0, ax=0, ay=0, az=0).patch()
|
||||||
coordinate_operation = {
|
coordinate_operation = {
|
||||||
"Eastings": self.e,
|
"Eastings": self.e,
|
||||||
"Northings": self.n,
|
"Northings": self.n,
|
||||||
@@ -90,7 +88,6 @@ class Patcher:
|
|||||||
self.file, projected_crs={"Name": self.name}, coordinate_operation=coordinate_operation
|
self.file, projected_crs={"Name": self.name}, coordinate_operation=coordinate_operation
|
||||||
)
|
)
|
||||||
OffsetObjectPlacements.Patcher(
|
OffsetObjectPlacements.Patcher(
|
||||||
self.src,
|
|
||||||
self.file,
|
self.file,
|
||||||
self.logger,
|
self.logger,
|
||||||
x=-self.x,
|
x=-self.x,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import typing
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src, file, logger, elevation: typing.Union[str, float] = "0"):
|
def __init__(self, file, logger, elevation: typing.Union[str, float] = "0"):
|
||||||
"""Sets the reference elevation of all IfcSites
|
"""Sets the reference elevation of all IfcSites
|
||||||
|
|
||||||
To completely reference model coordinates, a reference elevation should
|
To completely reference model coordinates, a reference elevation should
|
||||||
@@ -41,7 +41,6 @@ class Patcher:
|
|||||||
# All IfcSites will have their reference elevation set to 42.
|
# All IfcSites will have their reference elevation set to 42.
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetRefElevation", "arguments": [42]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetRefElevation", "arguments": [42]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.elevation = float(elevation)
|
self.elevation = float(elevation)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import typing
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src,
|
|
||||||
file,
|
file,
|
||||||
logger,
|
logger,
|
||||||
x: typing.Union[str, float] = "0",
|
x: typing.Union[str, float] = "0",
|
||||||
@@ -61,7 +60,6 @@ class Patcher:
|
|||||||
# Set the world coordinate system back to 0, 0, 0
|
# Set the world coordinate system back to 0, 0, 0
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetWorldCoordinateSystem", "arguments": [0,0,0,0,0,0]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetWorldCoordinateSystem", "arguments": [0,0,0,0,0,0]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.x = float(x)
|
self.x = float(x)
|
||||||
|
|||||||
@@ -24,18 +24,13 @@ from typing import Union
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
input_argument = "SUPPORTED"
|
def __init__(self, file: ifcopenshell.file, logger: logging.Logger, output_dir: Union[str, None] = None):
|
||||||
|
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: logging.Logger, output_dir: Union[str, None] = None):
|
|
||||||
"""Split an IFC model into multiple models based on building storey
|
"""Split an IFC model into multiple models based on building storey
|
||||||
|
|
||||||
The new IFC model names will be named after the storey name in the
|
The new IFC model names will be named after the storey name in the
|
||||||
format of {i}-{name}.ifc, where {i} is an ascending number starting from
|
format of {i}-{name}.ifc, where {i} is an ascending number starting from
|
||||||
0 and {name} is the name of the storey.
|
0 and {name} is the name of the storey.
|
||||||
|
|
||||||
`input` argument might be provided to ifcpatch - it will be used load file from disk
|
|
||||||
(otherwise `file` will be saved to a temporary file).
|
|
||||||
|
|
||||||
:param output_dir: Specifies an output directory where the new IFC models will be saved.
|
:param output_dir: Specifies an output directory where the new IFC models will be saved.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@@ -44,7 +39,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SplitByBuildingStorey", "arguments": ["C:/.../output_files"]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SplitByBuildingStorey", "arguments": ["C:/.../output_files"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.output_dir = output_dir
|
self.output_dir = output_dir
|
||||||
@@ -60,17 +54,14 @@ class Patcher:
|
|||||||
output_dir = Path(self.output_dir)
|
output_dir = Path(self.output_dir)
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
temp_file = None
|
temp_file = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False)
|
||||||
if not self.src:
|
self.file.write(temp_file.name)
|
||||||
temp_file = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False)
|
|
||||||
self.src = temp_file.name
|
|
||||||
self.file.write(self.src)
|
|
||||||
|
|
||||||
storeys = self.file.by_type("IfcBuildingStorey")
|
storeys = self.file.by_type("IfcBuildingStorey")
|
||||||
for i, storey in enumerate(storeys):
|
for i, storey in enumerate(storeys):
|
||||||
filename = f"{i}-{storey.Name}.ifc"
|
filename = f"{i}-{storey.Name}.ifc"
|
||||||
dest = filename if output_dir == None else output_dir / filename
|
dest = filename if output_dir == None else output_dir / filename
|
||||||
copyfile(self.src, dest)
|
copyfile(temp_file.name, dest)
|
||||||
old_ifc: ifcopenshell.file = ifcopenshell.open(dest)
|
old_ifc: ifcopenshell.file = ifcopenshell.open(dest)
|
||||||
new_ifc = ifcopenshell.file(schema=self.file.schema)
|
new_ifc = ifcopenshell.file(schema=self.file.schema)
|
||||||
|
|
||||||
@@ -98,9 +89,8 @@ class Patcher:
|
|||||||
new_ifc.remove(element)
|
new_ifc.remove(element)
|
||||||
new_ifc.write(dest)
|
new_ifc.write(dest)
|
||||||
|
|
||||||
if temp_file is not None:
|
temp_file.close()
|
||||||
temp_file.close()
|
os.unlink(temp_file.name)
|
||||||
os.unlink(temp_file.name)
|
|
||||||
|
|
||||||
def is_in_storey(self, element: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance) -> bool:
|
def is_in_storey(self, element: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance) -> bool:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from typing import Union
|
|||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
src: str,
|
|
||||||
file: ifcopenshell.file,
|
file: ifcopenshell.file,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
query: str = "IfcBeam",
|
query: str = "IfcBeam",
|
||||||
@@ -58,7 +57,6 @@ class Patcher:
|
|||||||
|
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "TessellateElements", "arguments": ["IfcBeam", False]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "TessellateElements", "arguments": ["IfcBeam", False]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.query = query
|
self.query = query
|
||||||
|
|||||||
@@ -25,18 +25,22 @@ from logging import Logger
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, query: str = ""):
|
def __init__(self, file: ifcopenshell.file, logger: Logger, query: str = ""):
|
||||||
"""Create independent copies for shared psets in IFC file.
|
"""Create independent copies for shared psets in IFC file.
|
||||||
|
|
||||||
In IFC it's possible that same property set is shared by multiple elements,
|
In IFC it's possible that same property set is shared by multiple
|
||||||
so editing it's properties will automatically change their values for all those elements.
|
elements, so editing it's properties will automatically change their
|
||||||
|
values for all those elements.
|
||||||
|
|
||||||
Sometimes it's intended but sometimes it's not and it's just the way some other
|
Sometimes it's intended but sometimes it's not and it's just the way
|
||||||
software exports IFC (e.g. there is a known case when Tekla exports shared psets for all the occurrences).
|
some other software exports IFC (e.g. there is a known case when Tekla
|
||||||
While it is more optimized way to store data, it may lead to unexpected results when editing properties.
|
exports shared psets for all the occurrences). While it is more
|
||||||
|
optimized way to store data, it may lead to unexpected results when
|
||||||
|
editing properties.
|
||||||
|
|
||||||
This recipe creates independent copies of all shared psets (may be limited by the query)
|
This recipe creates independent copies of all shared psets (may be
|
||||||
and assigns them to the elements, so they can be edited without affecting any other elements.
|
limited by the query) and assigns them to the elements, so they can be
|
||||||
|
edited without affecting any other elements.
|
||||||
|
|
||||||
:param query: A query to select the subset of IFC elements, optional.
|
:param query: A query to select the subset of IFC elements, optional.
|
||||||
If not provided, patch will be applied to all shared property sets in the model.
|
If not provided, patch will be applied to all shared property sets in the model.
|
||||||
@@ -51,7 +55,6 @@ class Patcher:
|
|||||||
# Unshare psets on all IfcWalls.
|
# Unshare psets on all IfcWalls.
|
||||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "UnsharePsets", "arguments": ["IfcWall"]})
|
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "UnsharePsets", "arguments": ["IfcWall"]})
|
||||||
"""
|
"""
|
||||||
self.src = src
|
|
||||||
self.file = file
|
self.file = file
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.query = query
|
self.query = query
|
||||||
|
|||||||
@@ -279,16 +279,13 @@ class Attribute(Facet):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> AttributeResult:
|
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> AttributeResult:
|
||||||
if self.cardinality == "optional":
|
|
||||||
return AttributeResult(True)
|
|
||||||
|
|
||||||
if isinstance(self.name, str):
|
if isinstance(self.name, str):
|
||||||
names = [self.name]
|
names = [self.name]
|
||||||
attribute_type = inst.wrapped_data.get_attribute_category(self.name)
|
attribute_type = inst.wrapped_data.get_attribute_category(self.name)
|
||||||
if attribute_type == 1: # Forward attribute
|
if attribute_type == 1: # Forward attribute
|
||||||
values = [getattr(inst, self.name, None)]
|
values = [getattr(inst, self.name, None)]
|
||||||
else:
|
else:
|
||||||
values = [None]
|
values = []
|
||||||
else:
|
else:
|
||||||
info = inst.get_info()
|
info = inst.get_info()
|
||||||
names = []
|
names = []
|
||||||
@@ -304,6 +301,8 @@ class Attribute(Facet):
|
|||||||
reason = None
|
reason = None
|
||||||
|
|
||||||
if not is_pass:
|
if not is_pass:
|
||||||
|
if self.cardinality == "optional":
|
||||||
|
return AttributeResult(True)
|
||||||
reason = {"type": "NOVALUE"}
|
reason = {"type": "NOVALUE"}
|
||||||
|
|
||||||
if is_pass:
|
if is_pass:
|
||||||
@@ -394,9 +393,6 @@ class Classification(Facet):
|
|||||||
return ifc_file.by_type("IfcObjectDefinition")
|
return ifc_file.by_type("IfcObjectDefinition")
|
||||||
|
|
||||||
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> ClassificationResult:
|
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> ClassificationResult:
|
||||||
if self.cardinality == "optional":
|
|
||||||
return ClassificationResult(True) # Is this really the correct behaviour?
|
|
||||||
|
|
||||||
leaf_references = ifcopenshell.util.classification.get_references(inst)
|
leaf_references = ifcopenshell.util.classification.get_references(inst)
|
||||||
|
|
||||||
references = leaf_references.copy()
|
references = leaf_references.copy()
|
||||||
@@ -407,6 +403,8 @@ class Classification(Facet):
|
|||||||
reason = None
|
reason = None
|
||||||
|
|
||||||
if not is_pass:
|
if not is_pass:
|
||||||
|
if self.cardinality == "optional":
|
||||||
|
return ClassificationResult(True)
|
||||||
reason = {"type": "NOVALUE"}
|
reason = {"type": "NOVALUE"}
|
||||||
|
|
||||||
if is_pass and self.value:
|
if is_pass and self.value:
|
||||||
@@ -657,9 +655,6 @@ class Property(Facet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> PropertyResult:
|
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> PropertyResult:
|
||||||
if self.cardinality == "optional":
|
|
||||||
return PropertyResult(True)
|
|
||||||
|
|
||||||
if isinstance(self.propertySet, str):
|
if isinstance(self.propertySet, str):
|
||||||
pset = get_pset(inst, self.propertySet)
|
pset = get_pset(inst, self.propertySet)
|
||||||
psets = {self.propertySet: pset} if pset else {}
|
psets = {self.propertySet: pset} if pset else {}
|
||||||
@@ -671,6 +666,8 @@ class Property(Facet):
|
|||||||
reason = None
|
reason = None
|
||||||
|
|
||||||
if not is_pass:
|
if not is_pass:
|
||||||
|
if self.cardinality == "optional":
|
||||||
|
return PropertyResult(True)
|
||||||
reason = {"type": "NOPSET"}
|
reason = {"type": "NOPSET"}
|
||||||
|
|
||||||
if is_pass:
|
if is_pass:
|
||||||
@@ -691,6 +688,8 @@ class Property(Facet):
|
|||||||
props[pset_name] = {k: v for k, v in pset_props.items() if k == self.baseName}
|
props[pset_name] = {k: v for k, v in pset_props.items() if k == self.baseName}
|
||||||
|
|
||||||
if not bool(props[pset_name]):
|
if not bool(props[pset_name]):
|
||||||
|
if self.cardinality == "optional":
|
||||||
|
return PropertyResult(True)
|
||||||
is_pass = False
|
is_pass = False
|
||||||
reason = {"type": "NOVALUE"}
|
reason = {"type": "NOVALUE"}
|
||||||
break
|
break
|
||||||
@@ -918,15 +917,14 @@ class Material(Facet):
|
|||||||
return ifc_file.by_type("IfcObjectDefinition")
|
return ifc_file.by_type("IfcObjectDefinition")
|
||||||
|
|
||||||
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> MaterialResult:
|
def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> MaterialResult:
|
||||||
if self.cardinality == "optional":
|
|
||||||
return MaterialResult(True)
|
|
||||||
|
|
||||||
material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True)
|
material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True)
|
||||||
|
|
||||||
is_pass = material is not None
|
is_pass = material is not None
|
||||||
reason = None
|
reason = None
|
||||||
|
|
||||||
if not is_pass:
|
if not is_pass:
|
||||||
|
if self.cardinality == "optional":
|
||||||
|
return MaterialResult(True)
|
||||||
reason = {"type": "NOVALUE"}
|
reason = {"type": "NOVALUE"}
|
||||||
|
|
||||||
if is_pass and self.value:
|
if is_pass and self.value:
|
||||||
@@ -1000,7 +998,17 @@ class Restriction:
|
|||||||
for constraint, value in self.options.items():
|
for constraint, value in self.options.items():
|
||||||
value = [value] if not isinstance(value, list) else value
|
value = [value] if not isinstance(value, list) else value
|
||||||
for v in value:
|
for v in value:
|
||||||
if constraint in ["length", "minLength", "maxLength"]:
|
if constraint in [
|
||||||
|
"length",
|
||||||
|
"minLength",
|
||||||
|
"maxLength",
|
||||||
|
"maxExclusive",
|
||||||
|
"maxInclusive",
|
||||||
|
"minExclusive",
|
||||||
|
"minInclusive",
|
||||||
|
"totalDigits",
|
||||||
|
"fractionDigits",
|
||||||
|
]:
|
||||||
value_dict = {"@value": v}
|
value_dict = {"@value": v}
|
||||||
else:
|
else:
|
||||||
value_dict = {"@value": str(v)}
|
value_dict = {"@value": str(v)}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user