mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecee3781e4 | |||
| 52f949b2dc | |||
| cb7e04f61a | |||
| b59e5b076d | |||
| 1516237537 | |||
| 53cd5f834e | |||
| 6dc6fddd0e | |||
| 52c8cc0a72 | |||
| 463473b959 | |||
| fd0c578995 | |||
| 74fbe94ed4 | |||
| 38002e1ee2 | |||
| 58b070d22d | |||
| e2b119bf9b | |||
| 8cf3807c2c | |||
| ad3d0fecb2 | |||
| fcfe6f97c3 | |||
| c0190b5065 | |||
| e596ac6b1b | |||
| d9ed6a4950 | |||
| a49ccfcfc8 | |||
| 7974d8a0d0 | |||
| f00e77ef6c | |||
| fc37cca1d0 | |||
| 53da00a5b7 | |||
| b5bc53bdf6 | |||
| bc0fbf744a | |||
| 53b26a9b66 | |||
| 858e73c9d9 | |||
| 952400bd58 | |||
| ffffcc576e | |||
| d8102d848d | |||
| 9407faf1f9 | |||
| 1e3f1e7dfc | |||
| 8aad28c730 | |||
| 81943ec9e5 | |||
| 5b15de50b6 | |||
| 4d50dce16d | |||
| 69b5d0940f | |||
| 273d6c1039 | |||
| 24ef03c96e | |||
| cc4553d3c9 | |||
| cf1f74e260 | |||
| 3052c6e578 | |||
| 0eb70b7fc3 | |||
| eb863bb2aa | |||
| 345a6e72d9 | |||
| 8b775ea30f | |||
| b712f30ad3 | |||
| 0bbe1aad5b | |||
| 29de264b43 | |||
| f4714d2012 | |||
| 8bfdabab80 | |||
| 50c1220730 | |||
| c280550e30 | |||
| 2bf303686c | |||
| bb91416c4e | |||
| 47e8b978af | |||
| 50eae4ae19 | |||
| 979183b64b | |||
| dc2f788326 | |||
| 7d12880eb9 | |||
| 6676c7f62e | |||
| c0e8c3d1b5 | |||
| e9245efd37 | |||
| de287395d2 | |||
| 938d72ebe3 | |||
| 605c86afc5 | |||
| 68cf8ee247 | |||
| a452d1d361 | |||
| 71067e4a14 | |||
| 7bacdbf8dd | |||
| 54c1dfdb77 | |||
| 834123ad14 | |||
| 3cbab975d5 | |||
| 0315542237 | |||
| 156cca5932 | |||
| 4672fc341f | |||
| f69b02cad9 | |||
| 40af3f0495 | |||
| 0dd42c1fb2 | |||
| 0990dfd372 | |||
| 9316e0bff0 | |||
| bc709f06f7 | |||
| cc031a23cb | |||
| 614b71e0c6 | |||
| 66329f07cb |
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Error Description and Steps to Reproduce**
|
||||
|
||||
<!--
|
||||
Describe what problem occurred and what you expected to happen instead.
|
||||
|
||||
1. To reproduce this, open file '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
-->
|
||||
|
||||
**Attachments**
|
||||
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
|
||||
-->
|
||||
|
||||
**Debug information**
|
||||
|
||||
<!--
|
||||
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
|
||||
-->
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Bug Report
|
||||
description: Crashes, error messages, and broken features
|
||||
type: bug
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug Description
|
||||
placeholder: |
|
||||
Describe what problem occurred and what you expected to happen instead.
|
||||
|
||||
1. To reproduce this, open file '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: "Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards. Please submit your report first then upload private files afterwards."
|
||||
placeholder: "If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload)."
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Debug and Error Output
|
||||
description: "If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc."
|
||||
render: yes
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Forums
|
||||
url: https://community.osarch.org/
|
||||
about: Have a question? Want to discuss an idea? Try the OSArch forums instead.
|
||||
- name: Live Chat
|
||||
url: https://osarch.org/chat
|
||||
about: Have a really confusing issue? Want real-time support?
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
**Feature Description**
|
||||
|
||||
<!--
|
||||
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
|
||||
-->
|
||||
@@ -0,0 +1,8 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement
|
||||
type: Feature
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature Description
|
||||
placeholder: "Describe a feature you'd like us to add, or a change to the user interface, design, or workflow for usability. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable."
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
||||
|
||||
# Download Blender.
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
jobs:
|
||||
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell' &&
|
||||
!startsWith(github.event.head_commit.message, 'Release ') &&
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- run: echo ok go
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
-
|
||||
name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
-
|
||||
name: Build ifcopenshell
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
make package
|
||||
working-directory: build
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Artifact name
|
||||
name: ifcos-artifacts
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
path: build/assets/Ifc*
|
||||
|
||||
deliver:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
name: Docker Build, Tag, Push
|
||||
|
||||
@@ -118,6 +118,6 @@ jobs:
|
||||
repository: aecgeeks/ifcopenshell
|
||||
# Since the dispatch is set to `tag`, `github.ref_name` should evaluate to the pushed tag
|
||||
# On a workflow dispatch, `ref_name` will take on the value from the dispatch payload
|
||||
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:latest' }}
|
||||
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:22.04' }}
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9].[0-9].[0-9]*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ src/bonsai/drawings
|
||||
src/bonsai/layouts
|
||||
|
||||
# ifcopenshell swig and compiled files
|
||||
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
|
||||
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
|
||||
|
||||
# apple
|
||||
|
||||
@@ -20,6 +20,7 @@ import os
|
||||
import bpy
|
||||
import bpy.utils.previews
|
||||
import importlib
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from . import handler, ui, prop, operator
|
||||
from typing import Callable, Union
|
||||
|
||||
@@ -221,7 +222,12 @@ def on_register(scene):
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
# Prevent crashes in Blender 4.4.0, see #6420.
|
||||
if issubclass(cls, (ImportHelper, ExportHelper)):
|
||||
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
|
||||
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(on_register)
|
||||
bpy.app.handlers.undo_post.append(handler.undo_post)
|
||||
bpy.app.handlers.redo_post.append(handler.redo_post)
|
||||
|
||||
@@ -47,7 +47,6 @@ class IfcExporter:
|
||||
self.set_header()
|
||||
IfcStore.update_cache()
|
||||
self.sync_all_objects()
|
||||
self.sync_edited_objects()
|
||||
tool.Project.save_linked_models_to_ifc()
|
||||
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
|
||||
if extension == "ifczip":
|
||||
@@ -101,41 +100,12 @@ class IfcExporter:
|
||||
result = self.sync_object_placement(obj)
|
||||
if result:
|
||||
results.append(result)
|
||||
result = self.sync_object_material(obj)
|
||||
# TODO: sync_object_material always returns None
|
||||
# so it's never really appended
|
||||
if result:
|
||||
results.append(result)
|
||||
except ReferenceError:
|
||||
pass # The object is likely deleted
|
||||
return results
|
||||
|
||||
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
|
||||
results: list[ifcopenshell.entity_instance] = []
|
||||
for obj in IfcStore.edited_objs.copy():
|
||||
if not obj:
|
||||
continue
|
||||
if not tool.Blender.is_valid_data_block(obj):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
results.append(element)
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
IfcStore.edited_objs.clear()
|
||||
return results
|
||||
|
||||
def sync_object_material(self, obj: bpy.types.Object) -> None:
|
||||
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
|
||||
return
|
||||
if not self.has_changed_materials(obj):
|
||||
return
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
|
||||
mprops = tool.Geometry.get_mesh_props(obj.data)
|
||||
checksum = mprops.material_checksum
|
||||
return checksum != tool.Geometry.get_material_checksum(obj)
|
||||
|
||||
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
|
||||
@@ -347,8 +347,8 @@ def load_post(scene):
|
||||
|
||||
# Bonsai overlays
|
||||
georeference_props = tool.Georeference.get_georeference_props()
|
||||
aggregate_props = bpy.context.scene.BIMAggregateProperties
|
||||
nest_props = bpy.context.scene.BIMNestProperties
|
||||
aggregate_props = tool.Aggregate.get_aggregate_props()
|
||||
nest_props = tool.Nest.get_nest_props()
|
||||
model_props = tool.Model.get_model_props()
|
||||
if georeference_props.should_visualise:
|
||||
GeoreferenceDecorator.install(bpy.context)
|
||||
@@ -361,7 +361,7 @@ def load_post(scene):
|
||||
if model_props.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
|
||||
if scene := bpy.context.scene:
|
||||
if preferences.should_use_snap and (scene := bpy.context.scene):
|
||||
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
|
||||
scene.tool_settings.use_snap = True
|
||||
# Match default Bonsai snaps
|
||||
|
||||
@@ -717,9 +717,15 @@ class IfcImporter:
|
||||
def create_structural_point_connections(self):
|
||||
for product in self.file.by_type("IfcStructuralPointConnection"):
|
||||
# TODO: make this based off ifcopenshell. See #1409
|
||||
representation: ifcopenshell.entity_instance = next(
|
||||
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
|
||||
)
|
||||
|
||||
representation = tool.Structural.get_vertex_representation(product)
|
||||
if not representation:
|
||||
print(
|
||||
"WARNING. Skipping invalid IfcStructuralPointConnection - "
|
||||
f"element has no valid representation:\n{product}."
|
||||
)
|
||||
continue
|
||||
|
||||
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
|
||||
if mesh is None:
|
||||
continue
|
||||
|
||||
@@ -20,6 +20,7 @@ import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
@@ -131,6 +132,9 @@ class AggregateDecorator:
|
||||
return shader
|
||||
|
||||
def draw_custom_batch(self, coords, color):
|
||||
indices = None
|
||||
if not tool.Blender.validate_shader_batch_data(coords, indices):
|
||||
return
|
||||
shader = self.dotted_line_shader()
|
||||
|
||||
arc_lengths = [0]
|
||||
@@ -141,6 +145,7 @@ class AggregateDecorator:
|
||||
shader,
|
||||
"LINE_STRIP",
|
||||
{"position": coords, "arcLength": arc_lengths},
|
||||
indices=indices,
|
||||
)
|
||||
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
@@ -150,13 +155,16 @@ class AggregateDecorator:
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_aggregate(self, context):
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if props.in_aggregate_mode:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
decorator_color_special = self.addon_prefs.decorator_color_special
|
||||
@@ -204,7 +212,7 @@ class AggregateDecorator:
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
if props.in_aggregate_mode:
|
||||
return
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
|
||||
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
||||
@@ -243,6 +251,8 @@ class AggregateModeDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -253,7 +263,7 @@ class AggregateModeDecorator:
|
||||
return
|
||||
region = context.region
|
||||
rv3d = region.data
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
|
||||
aggregate_obj = props.editing_aggregate
|
||||
if not aggregate_obj:
|
||||
@@ -284,7 +294,7 @@ class AggregateModeDecorator:
|
||||
def draw_aggregate_empty(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
return
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
aggregate_obj = props.editing_aggregate
|
||||
if not aggregate_obj:
|
||||
return
|
||||
|
||||
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.aggregate as core
|
||||
import bonsai.core.spatial
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -30,23 +31,30 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Assign Object To Aggregation"
|
||||
bl_description = (
|
||||
"Assign object as an aggregate to the selected IFC elements.\n\n"
|
||||
"If called from Bonsai UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
|
||||
"If called from Object Aggregates UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
|
||||
"If called directly, active object will be considered a relating whole"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
relating_object: bpy.props.IntProperty()
|
||||
related_object: bpy.props.IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
relating_object: int
|
||||
related_object: int
|
||||
|
||||
def _execute(self, context):
|
||||
relating_obj = None
|
||||
if self.relating_object:
|
||||
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
|
||||
assert relating_obj
|
||||
elif self.related_object:
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
|
||||
if aggregate:
|
||||
relating_obj = tool.Ifc.get_object(aggregate)
|
||||
elif context.active_object:
|
||||
assert relating_obj
|
||||
else:
|
||||
relating_obj = context.active_object
|
||||
|
||||
if not relating_obj:
|
||||
self.report({"ERROR"}, "No relating object is provided.")
|
||||
return
|
||||
@@ -66,7 +74,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
relating_obj=relating_obj,
|
||||
related_obj=obj,
|
||||
)
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
|
||||
new_editing_obj = props.editing_objects.add()
|
||||
new_editing_obj.obj = obj
|
||||
@@ -182,7 +190,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
|
||||
|
||||
def create_aggregate(self, context, ifc_class, aggregate_name):
|
||||
def create_aggregate(self, context: bpy.types.Context, ifc_class: str, aggregate_name: str) -> bpy.types.Object:
|
||||
aggregate = bpy.data.objects.new(aggregate_name, None)
|
||||
aggregate.location = context.scene.cursor.location
|
||||
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
|
||||
@@ -388,7 +396,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
objs = [o.obj for o in props.editing_objects]
|
||||
if props.in_aggregate_mode:
|
||||
if context.space_data.local_view:
|
||||
|
||||
@@ -33,6 +33,7 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
)
|
||||
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
||||
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
|
||||
@@ -98,12 +99,22 @@ class BIMObjectAggregateProperties(PropertyGroup):
|
||||
poll=poll_related_object,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
relating_object: Union[bpy.types.Object, None]
|
||||
related_object: Union[bpy.types.Object, None]
|
||||
|
||||
|
||||
class Objects(bpy.types.PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
|
||||
previous_hide_select: bpy.props.BoolProperty(default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
obj: Union[bpy.types.Object, None]
|
||||
previous_display_type: str
|
||||
previous_hide_select: bool
|
||||
|
||||
|
||||
class BIMAggregateProperties(PropertyGroup):
|
||||
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
|
||||
@@ -115,3 +126,10 @@ class BIMAggregateProperties(PropertyGroup):
|
||||
default=False,
|
||||
update=update_aggregate_decorator,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
in_aggregate_mode: bool
|
||||
editing_aggregate: Union[bpy.types.Object, None]
|
||||
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
aggregate_decorator: bool
|
||||
|
||||
@@ -49,11 +49,13 @@ class BIM_PT_aggregate(Panel):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.label(text="Aggregate Decorator")
|
||||
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
row.prop(props, "aggregate_decorator", icon="HIDE_OFF", text="")
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
props = context.active_object.BIMObjectAggregateProperties
|
||||
assert (obj := context.active_object)
|
||||
props = tool.Aggregate.get_object_aggregate_props(obj)
|
||||
|
||||
if props.is_editing:
|
||||
row = layout.row()
|
||||
@@ -131,9 +133,8 @@ class BIM_PT_linked_aggregate(Panel):
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMObjectAggregateProperties
|
||||
assert (obj := context.active_object)
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
row = layout.row(align=True)
|
||||
|
||||
if element.Decomposes:
|
||||
|
||||
@@ -45,6 +45,7 @@ from typing_extensions import assert_never
|
||||
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_alignment_csv"
|
||||
bl_label = "Import Alignment CSV"
|
||||
bl_description = " Import alignment from the provided .csv file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
|
||||
@@ -26,30 +26,42 @@ import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.attribute as core
|
||||
import bonsai.core.spatial
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def get_objs_for_operation(operator_properties, context):
|
||||
def get_objs_for_operation(
|
||||
operator_properties: "AttributesOperator", context: bpy.types.Context
|
||||
) -> list[bpy.types.Object]:
|
||||
if operator_properties.obj:
|
||||
return [bpy.data.objects[operator_properties.obj]]
|
||||
if operator_properties.mass_operation:
|
||||
return context.selected_objects[:]
|
||||
return [context.active_object]
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
return [obj]
|
||||
|
||||
|
||||
class EnableEditingAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_attributes"
|
||||
bl_label = "Enable Editing Attributes"
|
||||
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
class AttributesOperator:
|
||||
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
obj: str
|
||||
mass_operation: bool
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.mass_operation = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def enable_editing_attribute_on_obj(self, obj):
|
||||
props = obj.BIMAttributeProperties
|
||||
|
||||
class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
||||
bl_idname = "bim.enable_editing_attributes"
|
||||
bl_label = "Enable Editing Attributes"
|
||||
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def enable_editing_attribute_on_obj(self, obj: bpy.types.Object) -> None:
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
props.attributes.clear()
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -87,20 +99,14 @@ class EnableEditingAttributes(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingAttributes(bpy.types.Operator):
|
||||
class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
|
||||
bl_idname = "bim.disable_editing_attributes"
|
||||
bl_label = "Disable Editing Attributes"
|
||||
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.mass_operation = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def disable_editing_attributes_on_obj(self, obj):
|
||||
props = obj.BIMAttributeProperties
|
||||
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
props.is_editing_attributes = False
|
||||
|
||||
def execute(self, context):
|
||||
@@ -118,7 +124,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
obj = tool.Blender.get_active_object(is_selected=False)
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
if not obj or not (element := tool.Ifc.get_entity(obj)):
|
||||
return
|
||||
|
||||
def callback(attributes, prop):
|
||||
@@ -130,7 +136,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
attributes[prop.name] = None
|
||||
return True
|
||||
|
||||
props = obj.BIMAttributeProperties
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
|
||||
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
|
||||
|
||||
@@ -165,13 +171,12 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element.GlobalId = ifcopenshell.guid.new()
|
||||
|
||||
obj = context.active_object
|
||||
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
|
||||
if not obj or not (props := tool.Blender.get_object_attribute_props(obj)).is_editing_attributes:
|
||||
return {"FINISHED"}
|
||||
|
||||
props = obj.BIMAttributeProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if not element.is_a("IfcRoot"):
|
||||
if not element or not element.is_a("IfcRoot"):
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.use_selected and obj in context.selected_objects:
|
||||
@@ -191,7 +196,10 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
name: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
|
||||
obj = tool.Blender.get_active_object()
|
||||
assert obj
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
value = props.attributes[self.name].get_value()
|
||||
total = core.copy_attribute_to_selection(
|
||||
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
|
||||
)
|
||||
|
||||
@@ -29,8 +29,13 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class BIMAttributeProperties(PropertyGroup):
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
is_editing_attributes: BoolProperty(name="Is Editing Attributes")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
is_editing_attributes: bool
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.bim.helper
|
||||
import bpy.types
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.module.attribute.data import AttributesData
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def draw_ui(context, layout, attributes):
|
||||
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
|
||||
obj = context.active_object
|
||||
props = obj.BIMAttributeProperties
|
||||
assert obj
|
||||
props = tool.Blender.get_object_attribute_props(obj)
|
||||
|
||||
if props.is_editing_attributes:
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -43,6 +43,8 @@ class BoundaryDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -49,6 +49,8 @@ class ClashDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -536,6 +536,7 @@ class SelectCostScheduleProducts(bpy.types.Operator):
|
||||
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
bl_idname = "bim.import_cost_schedule_csv"
|
||||
bl_label = "Import Cost Schedule CSV"
|
||||
bl_description = "Import cost schdule from the provided .csv file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
|
||||
@@ -44,6 +44,7 @@ classes = (
|
||||
operator.SelectHighPolygonMeshes,
|
||||
operator.SelectHighestPolygonMeshes,
|
||||
operator.ToggleDetailedIOSLogs,
|
||||
operator.ValidateIfcAssets,
|
||||
operator.ValidateIfcFile,
|
||||
prop.BIMDebugProperties,
|
||||
ui.BIM_PT_debug,
|
||||
|
||||
@@ -37,6 +37,7 @@ import bonsai.core.profile
|
||||
import bonsai.core.type
|
||||
import bonsai.bim.handler
|
||||
import bonsai.bim.import_ifc as import_ifc
|
||||
from collections import defaultdict
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from pathlib import Path
|
||||
from bonsai import get_debug_info, format_debug_info
|
||||
@@ -152,6 +153,67 @@ class ValidateIfcFile(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ValidateIfcAssets(bpy.types.Operator):
|
||||
bl_idname = "bim.validate_ifc_assets"
|
||||
bl_label = "Validate IFC Assets"
|
||||
bl_description = (
|
||||
"Run Bonsai validation for IFC assets.\n\n"
|
||||
"There's an internal Bonsai convention to treat some IFC assets "
|
||||
"as unique based on their name (e.g. profiles, materials, styles).\n"
|
||||
"Though it's not required by IFC, it is a generally good practice "
|
||||
"to keep asset names unique and it also helps with various issues.\n"
|
||||
"If it's not conformed, it could lead to duplicated assets or "
|
||||
"the opposite - different assets of the same name treated as one."
|
||||
)
|
||||
bl_options = set()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("IFC file is not loaded.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
ifc_classes = {
|
||||
"IfcMaterial": "Name",
|
||||
"IfcProfileDef": "ProfileName",
|
||||
"IfcPresentationStyle": "Name",
|
||||
}
|
||||
|
||||
issues_found = False
|
||||
unique_assets: defaultdict[str, list[ifcopenshell.entity_instance]]
|
||||
for ifc_class, name_attr in ifc_classes.items():
|
||||
unique_assets = defaultdict(list)
|
||||
for asset in ifc_file.by_type(ifc_class):
|
||||
asset_name: Union[str, None] = getattr(asset, name_attr)
|
||||
if asset_name is None:
|
||||
continue
|
||||
unique_assets[asset_name].append(asset)
|
||||
|
||||
msg = ""
|
||||
for asset_name, assets in unique_assets.items():
|
||||
if len(assets) == 1:
|
||||
continue
|
||||
msg += f"{ifc_class} name '{asset_name}' is used by multiple assets:\n"
|
||||
for asset in assets:
|
||||
msg += f"- {asset}\n"
|
||||
|
||||
if msg:
|
||||
issues_found = True
|
||||
msg = f"Found issues validating {ifc_class} assets.\n" + msg
|
||||
print(msg)
|
||||
|
||||
if issues_found:
|
||||
self.report({"INFO"}, "Check asset validation results in the system console.")
|
||||
else:
|
||||
self.report({"INFO"}, "No asset validation issues found.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ProfileImportIFC(bpy.types.Operator):
|
||||
profile_filename = "blender.prof"
|
||||
bl_idname = "bim.profile_import_ifc"
|
||||
@@ -962,7 +1024,7 @@ class RestartBlender(bpy.types.Operator):
|
||||
if not ms_store_app_id:
|
||||
path = bpy.app.binary_path
|
||||
if platform.system() == "Windows":
|
||||
args = sys.argv[1:]
|
||||
args = sys.argv[1:]
|
||||
command_line = subprocess.list2cmdline([path] + args)
|
||||
os.execv(path, [command_line])
|
||||
else:
|
||||
|
||||
@@ -50,6 +50,8 @@ class BIM_PT_debug(Panel):
|
||||
row.prop(props, "package_name", text="")
|
||||
row.operator("bim.pip_install", icon="EVENT_PAGEDOWN").name = props.package_name
|
||||
|
||||
layout.operator("bim.validate_ifc_assets", icon="CHECKMARK")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.reload_ifc_file", text="Incrementally Reload Changes")
|
||||
|
||||
|
||||
@@ -318,6 +318,8 @@ class BaseDecorator:
|
||||
color: tuple[float, float, float, float],
|
||||
indices: Optional[list[tuple[int, int]]] = None,
|
||||
) -> None:
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.base_shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -1699,6 +1701,8 @@ class CutDecorator:
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -414,6 +414,8 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
|
||||
shader_wrapper = ExtrusionGuidesShader()
|
||||
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
|
||||
verts, edges = shader_wrapper.process_geometry(verts)
|
||||
if not tool.Blender.validate_shader_batch_data(verts, edges):
|
||||
verts, edges = [], []
|
||||
self.custom_shape = shader_wrapper, shader_wrapper.batch(
|
||||
pos=verts,
|
||||
indices=edges,
|
||||
|
||||
@@ -799,7 +799,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
exporter = bonsai.bim.export_ifc.IfcExporter(None)
|
||||
exporter.file = tool.Ifc.get()
|
||||
invalidated_elements = exporter.sync_all_objects()
|
||||
invalidated_elements += exporter.sync_edited_objects()
|
||||
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
|
||||
if cache := IfcStore.get_cache():
|
||||
[cache.remove(guid) for guid in invalidated_guids]
|
||||
@@ -824,6 +823,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
for link in props.links:
|
||||
if not link.is_loaded:
|
||||
continue
|
||||
if link.name not in IfcStore.session_files:
|
||||
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
|
||||
files[link.name] = IfcStore.session_files[link.name]
|
||||
@@ -2167,7 +2168,8 @@ class ActivateDrawingBase:
|
||||
camera_props = camera.data.BIMCameraProperties
|
||||
if camera_props.update_representation(camera):
|
||||
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
|
||||
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
# See 6452 and 6478.
|
||||
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -2221,6 +2223,7 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
|
||||
class SelectDocIfcFile(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.select_doc_ifc_file"
|
||||
bl_label = "Select Documentation IFC File"
|
||||
bl_description = "Selection .ifc file for documentation."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
|
||||
@@ -128,6 +128,8 @@ class ItemDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -1775,8 +1775,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.select_set(False)
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"IFC join failed - object '{obj}' has a different representation type "
|
||||
f"({obj_rep.RepresentationType}) than target '{self.target}' ({representation_type}).",
|
||||
f"IFC join failed - object '{obj.name}' has a different representation type "
|
||||
f"({obj_rep.RepresentationType})\nthan target '{self.target.name}' ({representation_type}).",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1929,7 +1929,7 @@ class OverrideEscape(bpy.types.Operator):
|
||||
tool.Geometry.disable_item_mode()
|
||||
elif tool.Model.get_model_props().openings:
|
||||
bpy.ops.bim.hide_all_openings()
|
||||
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
elif tool.Aggregate.get_aggregate_props().in_aggregate_mode:
|
||||
bpy.ops.bim.disable_aggregate_mode()
|
||||
elif active_object := context.active_object:
|
||||
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
|
||||
@@ -1953,24 +1953,26 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = context.selected_objects # Purposely exclude active object
|
||||
aprops = tool.Aggregate.get_aggregate_props()
|
||||
nprops = tool.Nest.get_nest_props()
|
||||
|
||||
if self.has_aggregates(selected_objs):
|
||||
if not context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
if not aprops.in_aggregate_mode:
|
||||
bonsai.core.aggregate.enable_aggregate_mode(tool.Aggregate, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.has_nests(selected_objs):
|
||||
if not context.scene.BIMNestProperties.in_nest_mode:
|
||||
if not nprops.in_nest_mode:
|
||||
bonsai.core.nest.enable_nest_mode(tool.Nest, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
if len(selected_objs) == 1 and context.active_object == selected_objs[0]:
|
||||
self.handle_single_object(context, context.active_object)
|
||||
elif len(selected_objs) == 0:
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
if aprops.in_aggregate_mode:
|
||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||
return {"FINISHED"}
|
||||
if context.scene.BIMNestProperties.in_nest_mode:
|
||||
if nprops.in_nest_mode:
|
||||
bonsai.core.nest.disable_nest_mode(tool.Nest)
|
||||
return {"FINISHED"}
|
||||
tool.Geometry.disable_item_mode()
|
||||
@@ -1981,9 +1983,10 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
pprops = tool.Project.get_project_props()
|
||||
aprops = tool.Aggregate.get_aggregate_props()
|
||||
if obj == props.representation_obj:
|
||||
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 aprops.not_editing_objects]:
|
||||
obj.select_set(False)
|
||||
self.report(
|
||||
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
|
||||
@@ -2099,7 +2102,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if (aggregate or parts) and not bpy.context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if (aggregate or parts) and not props.in_aggregate_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -2111,7 +2115,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
components = ifcopenshell.util.element.get_components(element)
|
||||
if (nest or components) and not bpy.context.scene.BIMNestProperties.in_nest_mode:
|
||||
props = tool.Nest.get_nest_props()
|
||||
if (nest or components) and not props.in_nest_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -2509,11 +2514,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert (obj := tool.Geometry.get_active_or_representation_obj())
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
representation_item = ifc_file.by_id(self.representation_item_id)
|
||||
tool.Geometry.remove_representation_item(representation_item)
|
||||
tool.Geometry.remove_representation_item(representation_item, element)
|
||||
tool.Geometry.reload_representation(obj)
|
||||
|
||||
# reload items ui
|
||||
@@ -3269,7 +3275,7 @@ class OverrideMove(bpy.types.Operator):
|
||||
self.new_active_obj = obj
|
||||
|
||||
# Get aggregates
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
aggregates_to_move = []
|
||||
for obj in context.selected_objects:
|
||||
@@ -3304,7 +3310,7 @@ class OverrideMove(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
# Get nests
|
||||
props = context.scene.BIMNestProperties
|
||||
props = tool.Nest.get_nest_props()
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
nests_to_move = []
|
||||
for obj in context.selected_objects:
|
||||
|
||||
@@ -283,7 +283,14 @@ class BIMGeometryProperties(PropertyGroup):
|
||||
is_changing_mode: BoolProperty(name="Is Changing Mode", default=False)
|
||||
mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode)
|
||||
representation_obj: PointerProperty(
|
||||
name="Representation Object", type=bpy.types.Object, update=update_representation_obj
|
||||
name="Representation Object",
|
||||
description=(
|
||||
"Only used for Item Mode. When element is in Item Mode, new objects are imported "
|
||||
"for each element's representation item, original element's object is hidden and "
|
||||
"representation_obj pointing to it. None if no object in Item Mode."
|
||||
),
|
||||
type=bpy.types.Object,
|
||||
update=update_representation_obj,
|
||||
)
|
||||
item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject)
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ class BIM_PT_representation_items(Panel):
|
||||
RepresentationItemsData.load()
|
||||
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
obj = props.representation_obj or tool.Blender.get_active_object()
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
props = tool.Geometry.get_object_geometry_props(obj)
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ class GeoreferenceDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
self.scale = props.visualization_scale
|
||||
content_pos = [v * self.scale for v in content_pos]
|
||||
|
||||
@@ -52,6 +52,8 @@ class SolarDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -40,6 +40,7 @@ from . import (
|
||||
railing,
|
||||
roof,
|
||||
mep,
|
||||
external,
|
||||
)
|
||||
from typing import NamedTuple
|
||||
|
||||
@@ -138,6 +139,7 @@ classes = (
|
||||
prop.BIMRoofProperties,
|
||||
prop.BIMPolylineProperties,
|
||||
prop.BIMProductPreviewProperties,
|
||||
prop.BIMExternalParametricGeometryProperties,
|
||||
ui.BIM_PT_array,
|
||||
ui.BIM_PT_stair,
|
||||
ui.BIM_PT_sverchok,
|
||||
@@ -147,6 +149,7 @@ classes = (
|
||||
ui.BIM_PT_roof,
|
||||
ui.BIM_MT_type_manager_menu,
|
||||
ui.BIM_MT_type_menu,
|
||||
ui.BIM_PT_external_parametric_geometry,
|
||||
ui.LaunchTypeMenu,
|
||||
ui.LaunchTypeManager,
|
||||
grid.BIM_OT_add_object,
|
||||
@@ -198,6 +201,7 @@ classes = (
|
||||
mep.MEPAddObstruction,
|
||||
mep.MEPAddTransition,
|
||||
mep.MEPAddBend,
|
||||
external.ApplyExternalParametricGeometry,
|
||||
)
|
||||
|
||||
addon_keymaps = []
|
||||
@@ -255,6 +259,9 @@ def register():
|
||||
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
|
||||
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
|
||||
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
|
||||
bpy.types.Object.BIMExternalParametricGeometryProperties = bpy.props.PointerProperty(
|
||||
type=prop.BIMExternalParametricGeometryProperties
|
||||
)
|
||||
|
||||
bpy.types.VIEW3D_MT_add.prepend(ui.add_menu)
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
@@ -277,6 +284,7 @@ def unregister():
|
||||
del bpy.types.Object.BIMDoorProperties
|
||||
del bpy.types.Object.BIMRailingProperties
|
||||
del bpy.types.Object.BIMRoofProperties
|
||||
del bpy.types.Object.BIMExternalParametricGeometryProperties
|
||||
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import blf
|
||||
import bpy
|
||||
@@ -34,6 +35,7 @@ from gpu_extras.presets import draw_circle_2d
|
||||
from typing import Union
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
from itertools import chain
|
||||
from typing import Union, Any
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
@@ -72,6 +74,8 @@ class ProfileDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -346,21 +350,28 @@ class PolylineDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
@classmethod
|
||||
def update(cls, event, tool_state, input_ui, snapping_point):
|
||||
def update(
|
||||
cls,
|
||||
event: bpy.types.Event,
|
||||
tool_state: tool.Polyline.ToolState,
|
||||
input_ui: tool.Polyline.PolylineUI,
|
||||
snapping_point: Vector,
|
||||
) -> None:
|
||||
cls.event = event
|
||||
cls.tool_state = tool_state
|
||||
cls.input_ui = input_ui
|
||||
|
||||
# TODO: unused?
|
||||
@classmethod
|
||||
def set_input_ui(cls, input_ui):
|
||||
def set_input_ui(cls, input_ui: tool.Polyline.PolylineUI) -> None:
|
||||
cls.input_ui = input_ui
|
||||
|
||||
@classmethod
|
||||
def set_angle_axis_line(cls, start, end):
|
||||
def set_angle_axis_line(cls, start: Vector, end: Vector) -> None:
|
||||
cls.axis_start = start
|
||||
cls.axis_end = end
|
||||
|
||||
def calculate_measurement_x_y_and_z(self, context):
|
||||
def calculate_measurement_x_y_and_z(self, context: bpy.types.Context) -> None:
|
||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
|
||||
@@ -383,7 +394,7 @@ class PolylineDecorator:
|
||||
return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle)
|
||||
|
||||
@classmethod
|
||||
def calculate_polygon(self, points):
|
||||
def calculate_polygon(cls, points: list[Vector]) -> dict[str, Any]:
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
@@ -405,12 +416,14 @@ class PolylineDecorator:
|
||||
return {"verts": verts, "edges": edges, "tris": tris}
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_input_ui(self, context):
|
||||
def draw_input_ui(self, context: bpy.types.Context) -> None:
|
||||
texts = {
|
||||
"D": "Distance: ",
|
||||
"A": "Angle: ",
|
||||
@@ -420,6 +433,7 @@ class PolylineDecorator:
|
||||
"AREA": "Area:",
|
||||
}
|
||||
try:
|
||||
assert self.event
|
||||
mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y
|
||||
except:
|
||||
mouse_pos = (None, None)
|
||||
@@ -534,7 +548,7 @@ class PolylineDecorator:
|
||||
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
|
||||
|
||||
# Area
|
||||
if measure_type == "AREA" and polyline_data.area:
|
||||
if measure_type == "POLY_AREA" and polyline_data.area:
|
||||
if len(polyline_verts) < 3:
|
||||
return
|
||||
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
|
||||
@@ -552,7 +566,7 @@ class PolylineDecorator:
|
||||
blf.draw(self.font_id, text)
|
||||
|
||||
# Length
|
||||
if measure_type in {"POLYLINE", "AREA"}:
|
||||
if measure_type in {"POLYLINE", "POLY_AREA"}:
|
||||
if len(polyline_verts) < 3:
|
||||
return
|
||||
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
|
||||
@@ -722,8 +736,8 @@ class PolylineDecorator:
|
||||
polyline_points = polyline_data.polyline_points
|
||||
else:
|
||||
polyline_points = []
|
||||
polyline_verts = []
|
||||
polyline_edges = []
|
||||
polyline_verts: list[Vector] = []
|
||||
polyline_edges: list[list[int]] = []
|
||||
for point_prop in polyline_points:
|
||||
point = Vector((point_prop.x, point_prop.y, point_prop.z))
|
||||
polyline_verts.append(point)
|
||||
@@ -761,7 +775,7 @@ class PolylineDecorator:
|
||||
# Area highlight
|
||||
if polyline_data:
|
||||
area = polyline_data.area.split(" ")[0]
|
||||
if area:
|
||||
if polyline_data.measurement_type == "POLY_AREA" and area:
|
||||
if float(area) > 0:
|
||||
tris = self.calculate_polygon(polyline_verts)["tris"]
|
||||
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
|
||||
@@ -815,6 +829,8 @@ class ProductDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -882,6 +898,8 @@ class WallAxisDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -946,6 +964,8 @@ class SlabDirectionDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -1001,6 +1021,8 @@ class FaceAreaDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @Andrej730
|
||||
#
|
||||
# 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 ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
class ApplyExternalParametricGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.apply_external_parametric_geometry"
|
||||
bl_label = "Apply External Parametric Geometry"
|
||||
bl_description = "Apply external parametric geometry to the active object."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
assert (active_representation := tool.Geometry.get_active_representation(obj))
|
||||
ifc_context = active_representation.ContextOfItems
|
||||
tool.Model.add_representation(obj, ifc_context)
|
||||
props = tool.Model.get_epg_props(obj)
|
||||
props.is_editing = False
|
||||
return {"FINISHED"}
|
||||
@@ -896,6 +896,8 @@ class DecorationsHandler:
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -550,6 +550,8 @@ class PolylineOperator:
|
||||
# TODO Fill doc strings
|
||||
""" """
|
||||
|
||||
objs_2d_bbox: list[tuple[bpy.types.Object, list[float]]]
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return context.space_data.type == "VIEW_3D"
|
||||
@@ -680,7 +682,10 @@ class PolylineOperator:
|
||||
for action, settings in instructions.items():
|
||||
if settings["icons"]:
|
||||
for key in settings["keys"]:
|
||||
self.layout.label(text="", icon=key)
|
||||
if bpy.app.version < (4, 3, 0) and key == "MOUSE_MMB_SCROLL":
|
||||
self.layout.label(text="MMB")
|
||||
else:
|
||||
self.layout.label(text="", icon=key)
|
||||
self.layout.label(text=action)
|
||||
else:
|
||||
key = settings["keys"][0]
|
||||
@@ -899,7 +904,8 @@ class PolylineOperator:
|
||||
if self.mousemove_count == 2:
|
||||
self.objs_2d_bbox = []
|
||||
for obj in self.visible_objs:
|
||||
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
|
||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||
self.objs_2d_bbox.append(bbox_2d)
|
||||
|
||||
if self.mousemove_count > 3:
|
||||
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
||||
@@ -988,7 +994,8 @@ class PolylineOperator:
|
||||
self.tool_state.mode = "Mouse"
|
||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||
for obj in self.visible_objs:
|
||||
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
|
||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||
self.objs_2d_bbox.append(bbox_2d)
|
||||
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
||||
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
|
||||
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
|
||||
|
||||
@@ -1142,3 +1142,43 @@ class BIMProductPreviewProperties(PropertyGroup):
|
||||
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
|
||||
|
||||
def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.is_editing:
|
||||
return
|
||||
|
||||
tool.Model.clean_up_parametric_geometry(self.id_data)
|
||||
del self["is_editing"]
|
||||
del self["geo_nodes"]
|
||||
|
||||
|
||||
def update_geo_nodes(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.geo_nodes:
|
||||
tool.Model.setup_parametric_geometry(self.id_data)
|
||||
return
|
||||
|
||||
modifier = tool.Model.get_epg_modifier(self.id_data)
|
||||
assert modifier
|
||||
modifier.show_viewport = False
|
||||
del self["geo_nodes"]
|
||||
|
||||
|
||||
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(
|
||||
name="Is Editing Paramteric Geometry",
|
||||
description="Toggle editing parametric geometry.",
|
||||
default=False,
|
||||
update=update_is_editing,
|
||||
)
|
||||
geo_nodes: bpy.props.PointerProperty(
|
||||
name="Geometry Nodes",
|
||||
description="Geometry nodes tree to use as a source for representation.",
|
||||
type=bpy.types.GeometryNodeTree,
|
||||
update=update_geo_nodes,
|
||||
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
geo_nodes: Union[bpy.types.GeometryNodeTree, None]
|
||||
|
||||
@@ -677,8 +677,9 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# Restore the position to before it was changed by the offset and x_angle
|
||||
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
offset = Vector((0.0, 0.0, -layer_params["offset"]))
|
||||
rot_offset = offset @ rot_matrix
|
||||
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
|
||||
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
|
||||
rot_offset = offset_vector @ rot_matrix
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
@@ -725,8 +726,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# Restore the position to after it was changed by the offset and x_angle
|
||||
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
|
||||
offset = Vector((0.0, 0.0, -layer_params["offset"]))
|
||||
rot_offset = offset @ rot_matrix
|
||||
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
|
||||
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
|
||||
rot_offset = offset_vector @ rot_matrix
|
||||
tranlation_matrix = Matrix.Translation(rot_offset)
|
||||
position = position @ tranlation_matrix
|
||||
|
||||
|
||||
@@ -701,6 +701,49 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
row.operator("bim.add_roof", icon="ADD", text="")
|
||||
|
||||
|
||||
class BIM_PT_external_parametric_geometry(bpy.types.Panel):
|
||||
bl_label = "External Parametric Geometry"
|
||||
bl_idname = "BIM_PT_external_parametric_geometry"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_parent_id = "BIM_PT_tab_parametric_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (obj := context.active_object) and obj.type == "MESH"
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
layout = self.layout
|
||||
assert layout
|
||||
props = tool.Model.get_epg_props(obj)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
if not props.is_editing:
|
||||
row.prop(props, "is_editing", icon="GREASEPENCIL", text="")
|
||||
return
|
||||
|
||||
row.operator("bim.apply_external_parametric_geometry", icon="CHECKMARK", text="")
|
||||
row.prop(props, "is_editing", icon="CANCEL", text="")
|
||||
row = layout.row(align=True)
|
||||
row.prop_search(props, "geo_nodes", bpy.data, "node_groups")
|
||||
if props.geo_nodes:
|
||||
assert (modifier := tool.Model.get_epg_modifier(obj))
|
||||
inputs = tool.Model.get_parametric_geometry_inputs(modifier)
|
||||
# NOTE: users won't be able to see inputs descriptions.
|
||||
# If we add group node inputs as modifiers inputs, descriptions will be visible.
|
||||
# But then we need to ensure inputs are up to date
|
||||
# (e.g. probably just by adding a refresh button).
|
||||
for input in inputs:
|
||||
row = layout.row(align=True)
|
||||
row.prop(input, "default_value", text=input.name)
|
||||
|
||||
|
||||
def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
|
||||
self.layout.operator_context = "INVOKE_REGION_WIN"
|
||||
self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
|
||||
|
||||
@@ -24,10 +24,12 @@ import math
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.type
|
||||
import mathutils.geometry
|
||||
import bonsai.core.type
|
||||
@@ -52,7 +54,10 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
|
||||
@@ -66,7 +71,7 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
slab = None
|
||||
walls = []
|
||||
walls: list[bpy.types.Object] = []
|
||||
if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)):
|
||||
slab = obj
|
||||
for obj in tool.Blender.get_selected_objects(include_active=False):
|
||||
@@ -137,11 +142,17 @@ class AlignWall(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
return context.active_object and len(selected_valid_objects) > 1
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected.")
|
||||
return False
|
||||
selected_valid_objects = tool.Model.get_selected_mesh_objects()
|
||||
if len(selected_valid_objects) < 2:
|
||||
cls.poll_message_set("Please select at least two mesh objects.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
selected_objects = tool.Model.get_selected_mesh_objects()
|
||||
for obj in selected_objects:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
@@ -165,10 +176,13 @@ class FlipWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
selected_objs = tool.Model.get_selected_mesh_objects()
|
||||
joiner = DumbWallJoiner()
|
||||
for obj in selected_objs:
|
||||
joiner.flip(obj)
|
||||
@@ -185,10 +199,13 @@ class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
selected_objs = tool.Model.get_selected_mesh_objects()
|
||||
for obj in selected_objs:
|
||||
DumbWallJoiner().split(obj, context.scene.cursor.location)
|
||||
return {"FINISHED"}
|
||||
@@ -202,12 +219,23 @@ class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected.")
|
||||
return False
|
||||
elif not tool.Model.has_selected_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
mesh_objects = [o for o in tool.Model.get_selected_ifc_objects() if o.type == "MESH"]
|
||||
if len(mesh_objects) != 2:
|
||||
cls.poll_message_set("Please select exactly two mesh IFC objects.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
||||
if len(selected_objs) == 2:
|
||||
DumbWallJoiner().merge([o for o in selected_objs if o != context.active_object][0], context.active_object)
|
||||
active_obj = context.active_object
|
||||
assert active_obj
|
||||
selected_objs = tool.Model.get_selected_mesh_objects()
|
||||
DumbWallJoiner().merge(next(o for o in selected_objs if o != active_obj), active_obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -218,50 +246,59 @@ class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
DumbWallRecalculator().recalculate(context.selected_objects)
|
||||
objects = tool.Model.get_selected_mesh_ifc_objects()
|
||||
DumbWallRecalculator().recalculate(objects)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_extrusion_depth"
|
||||
bl_label = "Update"
|
||||
bl_description = "Update Height"
|
||||
bl_description = "Update height for the selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
depth: bpy.props.FloatProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
layer2_objs = []
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for obj in context.selected_objects:
|
||||
layer2_objs: list[bpy.types.Object] = []
|
||||
ifc_file = tool.Ifc.get()
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
|
||||
for obj in selected_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
assert element
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
continue
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
continue
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements":
|
||||
ifcopenshell.api.run(
|
||||
"geometry.disconnect_element",
|
||||
tool.Ifc.get(),
|
||||
ifcopenshell.api.geometry.disconnect_element(
|
||||
ifc_file,
|
||||
relating_element=rel.RelatingElement,
|
||||
related_element=element,
|
||||
)
|
||||
layer2_objs.append(obj)
|
||||
|
||||
if layer2_objs:
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
@@ -270,30 +307,33 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_extrusion_x_angle"
|
||||
bl_label = "Update"
|
||||
bl_description = "Update Angle"
|
||||
bl_description = "Update angle for the selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
layer2_objs = []
|
||||
other_objs = []
|
||||
layer2_objs: list[bpy.types.Object] = []
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle
|
||||
x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for obj in context.selected_objects:
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
|
||||
for obj in selected_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
assert element
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
continue
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
continue
|
||||
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
|
||||
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
|
||||
@@ -380,17 +420,21 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_layer_length"
|
||||
bl_label = "Update"
|
||||
bl_description = "Update Length"
|
||||
bl_description = "Update length for the selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
length: bpy.props.FloatProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
if not tool.Model.has_selected_mesh_ifc_objects():
|
||||
cls.poll_message_set("No mesh IFC objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
joiner = DumbWallJoiner()
|
||||
for obj in context.selected_objects:
|
||||
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
|
||||
for obj in selected_objs:
|
||||
joiner.set_length(obj, self.length)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1078,7 +1122,7 @@ class DumbWallJoiner:
|
||||
)
|
||||
self.recreate_wall(element1, wall1)
|
||||
|
||||
def merge(self, wall1, wall2):
|
||||
def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
if tool.Ifc.is_moved(wall1):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
|
||||
if tool.Ifc.is_moved(wall2):
|
||||
@@ -1086,6 +1130,7 @@ class DumbWallJoiner:
|
||||
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(wall2)
|
||||
assert element1 and element2
|
||||
|
||||
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
|
||||
p3, p4 = ifcopenshell.util.representation.get_reference_line(element2)
|
||||
@@ -1192,14 +1237,13 @@ class DumbWallJoiner:
|
||||
self.set_axis(element1, intersect, p2)
|
||||
self.recreate_wall(element1, wall1)
|
||||
|
||||
def set_length(self, wall1, si_length):
|
||||
def set_length(self, wall1: bpy.types.Object, si_length: float) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
if not element1:
|
||||
return
|
||||
assert element1
|
||||
if tool.Ifc.is_moved(wall1):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
|
||||
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
|
||||
ifcopenshell.api.geometry.disconnect_path(tool.Ifc.get(), element=element1, connection_type="ATEND")
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
|
||||
@@ -1207,7 +1251,7 @@ class DumbWallJoiner:
|
||||
self.set_axis(element1, p1, p2)
|
||||
self.recreate_wall(element1, wall1)
|
||||
|
||||
def join_T(self, wall1, wall2):
|
||||
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(wall2)
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
@@ -1219,8 +1263,7 @@ class DumbWallJoiner:
|
||||
return
|
||||
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.connect_path",
|
||||
ifcopenshell.api.geometry.connect_path(
|
||||
tool.Ifc.get(),
|
||||
related_element=element1,
|
||||
relating_element=element2,
|
||||
@@ -1231,7 +1274,7 @@ class DumbWallJoiner:
|
||||
|
||||
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
||||
|
||||
def connect(self, obj1, obj2):
|
||||
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
|
||||
wall1 = tool.Ifc.get_entity(obj1)
|
||||
wall2 = tool.Ifc.get_entity(obj2)
|
||||
if tool.Ifc.is_moved(obj1):
|
||||
|
||||
@@ -516,6 +516,7 @@ class CreateObjectUI:
|
||||
@classmethod
|
||||
def draw_type_manager_launcher(cls, context):
|
||||
ui_context = context.region.type
|
||||
props = tool.Model.get_model_props()
|
||||
row = cls.layout.row(align=True)
|
||||
box = cls.layout.box()
|
||||
row1 = box.row(align=True)
|
||||
@@ -716,12 +717,14 @@ class EditObjectUI:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
|
||||
if context.region.type == "TOOL_HEADER":
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
aprops = tool.Aggregate.get_aggregate_props()
|
||||
if aprops.in_aggregate_mode:
|
||||
layout.label(text=f"Aggregate Mode", icon="EMPTY_AXIS")
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator("bim.disable_aggregate_mode", text="", icon="X")
|
||||
op = row.operator("bim.toggle_aggregate_mode_local_view", text="", icon="ZOOM_SELECTED")
|
||||
if context.scene.BIMNestProperties.in_nest_mode:
|
||||
nprops = tool.Nest.get_nest_props()
|
||||
if nprops.in_nest_mode:
|
||||
layout.label(text=f"Nest Mode", icon="EMPTY_AXIS")
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator("bim.disable_nest_mode", text="", icon="X")
|
||||
@@ -909,7 +912,9 @@ class EditObjectUI:
|
||||
else:
|
||||
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(cls.layout, "Extend To Undersideb", "S_E", "", ui_context)
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_to_underside.__doc__, ui_context
|
||||
)
|
||||
|
||||
if AuthoringData.data["is_flippable_element"]:
|
||||
cls.draw_flip(ui_context, row)
|
||||
@@ -959,6 +964,13 @@ class EditObjectUI:
|
||||
op_text = "Add Void" if ui_context != "TOOL_HEADER" else ""
|
||||
op_icon = custom_icon_previews["ADD_VOID"].icon_id
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
op = row.operator("bim.add_element", text=op_text, icon_value=op_icon)
|
||||
op.ifc_product = "IfcFeatureElement"
|
||||
op.ifc_class = "IfcOpeningElement"
|
||||
op.skip_dialog = True
|
||||
if ui_context != "TOOL_HEADER":
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
|
||||
if AuthoringData.data["is_voidable_element"]:
|
||||
if AuthoringData.data["has_visible_openings"]:
|
||||
@@ -981,12 +993,19 @@ class EditObjectUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.separator()
|
||||
row.label(text="Align") if ui_context != "TOOL_HEADER" else row
|
||||
|
||||
description: str
|
||||
if AuthoringData.data["active_material_usage"] == "LAYER2":
|
||||
description = bpy.ops.bim.align_wall.__doc__
|
||||
else:
|
||||
description = bpy.ops.bim.align_product.__doc__
|
||||
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Exterior", "S_X", "", ui_context)
|
||||
add_layout_hotkey_operator(row, "Exterior", "S_X", description, ui_context)
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Centreline", "S_C", "", ui_context)
|
||||
add_layout_hotkey_operator(row, "Centreline", "S_C", description, ui_context)
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Interior", "S_V", "", ui_context)
|
||||
add_layout_hotkey_operator(row, "Interior", "S_V", description, ui_context)
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__, ui_context)
|
||||
|
||||
@@ -1334,8 +1353,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.add_boundary()
|
||||
|
||||
def hotkey_S_O(self):
|
||||
if len(bpy.context.selected_objects) == 2:
|
||||
if len(bpy.context.selected_objects) > 1:
|
||||
bpy.ops.bim.add_opening()
|
||||
else:
|
||||
bpy.ops.bim.add_element(
|
||||
"INVOKE_DEFAULT", ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement", skip_dialog=True
|
||||
)
|
||||
|
||||
def hotkey_S_L(self):
|
||||
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
|
||||
|
||||
@@ -20,6 +20,7 @@ import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
@@ -131,6 +132,10 @@ class NestDecorator:
|
||||
return shader
|
||||
|
||||
def draw_custom_batch(self, coords, color):
|
||||
indices = None
|
||||
if not tool.Blender.validate_shader_batch_data(coords, indices):
|
||||
return
|
||||
|
||||
shader = self.dotted_line_shader()
|
||||
|
||||
arc_lengths = [0]
|
||||
@@ -141,6 +146,7 @@ class NestDecorator:
|
||||
shader,
|
||||
"LINE_STRIP",
|
||||
{"position": coords, "arcLength": arc_lengths},
|
||||
indices=indices,
|
||||
)
|
||||
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
@@ -150,13 +156,16 @@ class NestDecorator:
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_nest(self, context):
|
||||
if context.scene.BIMNestProperties.in_nest_mode:
|
||||
def draw_nest(self, context: bpy.types.Context) -> None:
|
||||
props = tool.Nest.get_nest_props()
|
||||
if props.in_nest_mode:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
decorator_color_special = self.addon_prefs.decorator_color_special
|
||||
@@ -243,6 +252,8 @@ class NestModeDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -253,7 +264,7 @@ class NestModeDecorator:
|
||||
return
|
||||
region = context.region
|
||||
rv3d = region.data
|
||||
props = context.scene.BIMNestProperties
|
||||
props = tool.Nest.get_nest_props()
|
||||
|
||||
aggregate_obj = props.editing_nest
|
||||
if not aggregate_obj:
|
||||
@@ -282,7 +293,7 @@ class NestModeDecorator:
|
||||
def draw_nest_empty(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
return
|
||||
props = context.scene.BIMNestProperties
|
||||
props = tool.Nest.get_nest_props()
|
||||
nest_obj = props.editing_nest
|
||||
if not nest_obj:
|
||||
return
|
||||
|
||||
@@ -158,7 +158,7 @@ class BIM_OT_toggle_nest_mode_local_view(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMNestProperties
|
||||
props = tool.Nest.get_nest_props()
|
||||
objs = [o.obj for o in props.editing_objects]
|
||||
if props.in_nest_mode:
|
||||
if context.space_data.local_view:
|
||||
|
||||
@@ -32,10 +32,11 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
)
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator, NestModeDecorator
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
|
||||
def update_relating_object(self, context):
|
||||
def message(self, context):
|
||||
def update_relating_object(self: "BIMObjectNestProperties", context: bpy.types.Context) -> None:
|
||||
def message(self, context: bpy.types.Context) -> None:
|
||||
self.layout.label(text="Please select a valid Ifc Element")
|
||||
|
||||
if self.relating_object is None:
|
||||
@@ -49,15 +50,19 @@ class BIMObjectNestProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_object: PointerProperty(name="Nest Host", type=bpy.types.Object, update=update_relating_object)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
relating_object: bpy.types.Object
|
||||
|
||||
def update_nest_decorator(self, context):
|
||||
|
||||
def update_nest_decorator(self: "BIMNestProperties", context: bpy.types.Context) -> None:
|
||||
if self.nest_decorator:
|
||||
NestDecorator.install(bpy.context)
|
||||
else:
|
||||
NestDecorator.uninstall()
|
||||
|
||||
|
||||
def update_nest_mode_decorator(self, context):
|
||||
def update_nest_mode_decorator(self: "BIMNestProperties", context: bpy.types.Context) -> None:
|
||||
if self.in_nest_mode:
|
||||
NestModeDecorator.install(bpy.context)
|
||||
else:
|
||||
@@ -68,6 +73,10 @@ class Objects(bpy.types.PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
obj: Union[bpy.types.Object, None]
|
||||
previous_display_type: str
|
||||
|
||||
|
||||
class BIMNestProperties(PropertyGroup):
|
||||
in_nest_mode: BoolProperty(name="In Edit Mode", update=update_nest_mode_decorator)
|
||||
@@ -79,3 +88,10 @@ class BIMNestProperties(PropertyGroup):
|
||||
default=False,
|
||||
update=update_nest_decorator,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
in_nest_mode: bool
|
||||
editing_nest: Union[bpy.types.Object, None]
|
||||
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
|
||||
nest_decorator: bool
|
||||
|
||||
@@ -48,7 +48,8 @@ class BIM_PT_nest(Panel):
|
||||
if not NestData.is_loaded:
|
||||
NestData.load()
|
||||
|
||||
props = context.active_object.BIMObjectNestProperties
|
||||
assert (obj := context.active_object)
|
||||
props = tool.Nest.get_object_nest_props(obj)
|
||||
|
||||
if props.is_editing:
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -16,10 +16,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.helper import draw_attributes
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import Attribute
|
||||
|
||||
|
||||
class BIM_PT_patch(bpy.types.Panel):
|
||||
@@ -54,8 +59,8 @@ class BIM_PT_patch(bpy.types.Panel):
|
||||
row.prop(props, "ifc_patch_output")
|
||||
row.operator("bim.select_ifc_patch_output", icon="FILE_FOLDER", text="")
|
||||
|
||||
def draw_callback_(_, row: bpy.types.UILayout) -> None:
|
||||
if props.ifc_patch_recipes == "ExtractElements":
|
||||
def draw_callback_(attribute: Attribute, row: bpy.types.UILayout) -> None:
|
||||
if props.ifc_patch_recipes == "ExtractElements" and attribute.name == "Query":
|
||||
row.operator("bim.patch_query_from_selected", text="", icon="EYEDROPPER")
|
||||
|
||||
if props.ifc_patch_args_attr:
|
||||
|
||||
@@ -68,6 +68,8 @@ class ProjectDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -137,6 +139,8 @@ class ClippingPlaneDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -242,6 +246,8 @@ class MeasureDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
@@ -323,7 +329,7 @@ class MeasureDecorator:
|
||||
|
||||
all_positions = []
|
||||
for i in range(len(polyline_points)):
|
||||
if i < 1 and measure_type == "AREA":
|
||||
if i < 1 and measure_type == "POLY_AREA":
|
||||
continue
|
||||
if i == 0:
|
||||
continue
|
||||
@@ -368,7 +374,7 @@ class MeasureDecorator:
|
||||
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
|
||||
|
||||
# Area
|
||||
if measure_type == "AREA" and polyline_data.area:
|
||||
if measure_type == "POLY_AREA" and polyline_data.area:
|
||||
if len(polyline_verts) < 3:
|
||||
continue
|
||||
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
|
||||
@@ -386,7 +392,7 @@ class MeasureDecorator:
|
||||
blf.draw(self.font_id, text)
|
||||
|
||||
# Length
|
||||
if measure_type in {"POLYLINE", "AREA"}:
|
||||
if measure_type in {"POLYLINE", "POLY_AREA"}:
|
||||
if len(polyline_verts) < 3:
|
||||
continue
|
||||
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
|
||||
@@ -449,7 +455,7 @@ class MeasureDecorator:
|
||||
# Area highlight
|
||||
if polyline_data:
|
||||
area = polyline_data.area.split(" ")[0]
|
||||
if area:
|
||||
if polyline_data.measurement_type == "POLY_AREA" and area:
|
||||
if float(area) > 0:
|
||||
tris = self.calculate_polygon(polyline_verts)["tris"]
|
||||
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
|
||||
|
||||
@@ -1304,6 +1304,9 @@ class LoadLink(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
filepath = Path(tool.Ifc.resolve_uri(self.filepath))
|
||||
if not filepath.exists():
|
||||
self.report({"ERROR"}, f"File does not exist: '{filepath}'")
|
||||
return {"CANCELLED"}
|
||||
self.filepath_ = filepath
|
||||
if filepath.suffix.lower().endswith(".blend"):
|
||||
self.link_blend(filepath)
|
||||
@@ -2346,7 +2349,9 @@ class RefreshClippingPlanes(bpy.types.Operator):
|
||||
data = region.data
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
if not len(props.clipping_planes) and not self.camera:
|
||||
# See 6452 and 6478.
|
||||
# if not len(props.clipping_planes) and not self.camera:
|
||||
if not len(props.clipping_planes):
|
||||
data.use_clip_planes = False
|
||||
else:
|
||||
with bpy.context.temp_override(area=area, region=region):
|
||||
@@ -2375,13 +2380,14 @@ class RefreshClippingPlanes(bpy.types.Operator):
|
||||
clip_planes.append(clip_plane)
|
||||
bm.free()
|
||||
|
||||
if self.camera:
|
||||
normal = self.camera.matrix_world.col[2].to_3d()
|
||||
normal *= -1
|
||||
center = self.camera.matrix_world.translation
|
||||
distance = -center.dot(normal)
|
||||
clip_plane = (normal.x, normal.y, normal.z, distance)
|
||||
clip_planes.append(clip_plane)
|
||||
# See 6452 and 6478.
|
||||
# if self.camera:
|
||||
# normal = self.camera.matrix_world.col[2].to_3d()
|
||||
# normal *= -1
|
||||
# center = self.camera.matrix_world.translation
|
||||
# distance = -center.dot(normal)
|
||||
# clip_plane = (normal.x, normal.y, normal.z, distance)
|
||||
# clip_planes.append(clip_plane)
|
||||
|
||||
clip_planes = cycle(clip_planes)
|
||||
data.clip_planes = [tuple(next(clip_planes)) for i in range(0, 6)]
|
||||
|
||||
@@ -35,6 +35,7 @@ import bonsai.bim.module.root.prop as root_prop
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.helper import get_enum_items, prop_with_search
|
||||
from mathutils import Vector
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class EnableReassignClass(bpy.types.Operator):
|
||||
@@ -173,24 +174,35 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_class"
|
||||
bl_label = "Assign IFC Class"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Assign the IFC Class to the selected objects"
|
||||
bl_description = "Assign the IFC Class to the selected non-ifc objects."
|
||||
obj: bpy.props.StringProperty()
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
predefined_type: bpy.props.StringProperty()
|
||||
userdefined_type: bpy.props.StringProperty()
|
||||
context_id: bpy.props.IntProperty()
|
||||
|
||||
# TODO: is never used?
|
||||
should_add_representation: bpy.props.BoolProperty(default=True)
|
||||
|
||||
ifc_representation_class: bpy.props.StringProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
obj: str
|
||||
ifc_class: str
|
||||
predefined_type: str
|
||||
userdefined_type: str
|
||||
context_id: int
|
||||
should_add_representation: bool
|
||||
ifc_representation_class: str
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Root.get_root_props()
|
||||
objects: list[bpy.types.Object] = []
|
||||
if self.obj:
|
||||
objects = [bpy.data.objects[self.obj]]
|
||||
elif objects := context.selected_objects:
|
||||
pass
|
||||
elif obj := context.active_object:
|
||||
objects = [obj]
|
||||
else:
|
||||
objects = list(tool.Blender.get_selected_objects())
|
||||
|
||||
if not objects:
|
||||
self.report({"INFO"}, "No objects selected.")
|
||||
@@ -203,8 +215,20 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifc_context = int(props.contexts or "0") or None
|
||||
if ifc_context:
|
||||
ifc_context = tool.Ifc.get().by_id(ifc_context)
|
||||
active_object = context.active_object
|
||||
|
||||
schema = ifcopenshell.schema_by_name(ifc_file.schema)
|
||||
declaration = schema.declaration_by_name(ifc_class)
|
||||
is_structural = ifcopenshell.util.schema.is_a(declaration, "IfcStructuralItem")
|
||||
|
||||
# Manage selection as operator can be called not from UI but using `object` argument.
|
||||
current_selection = tool.Blender.get_objects_selection(context)
|
||||
tool.Blender.clear_objects_selection()
|
||||
|
||||
for obj in objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
continue
|
||||
|
||||
if obj.mode != "OBJECT":
|
||||
self.report({"ERROR"}, "Object must be in OBJECT mode to assign class")
|
||||
continue
|
||||
@@ -218,13 +242,41 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
continue
|
||||
|
||||
if self.should_add_representation and isinstance(obj.data, bpy.types.Mesh) and obj.data.polygons:
|
||||
if (
|
||||
self.should_add_representation
|
||||
and not is_structural
|
||||
and isinstance(obj.data, bpy.types.Mesh)
|
||||
and obj.data.polygons
|
||||
):
|
||||
# Export mesh as tesselation.
|
||||
|
||||
def ensure_single_user_mesh(mesh: bpy.types.Mesh) -> None:
|
||||
if mesh.users == 1:
|
||||
return
|
||||
obj.select_set(True)
|
||||
# temp_override is not supported.
|
||||
bpy.ops.object.make_single_user(
|
||||
object=True, obdata=True, material=False, animation=False, obdata_animation=False
|
||||
)
|
||||
obj.select_set(False)
|
||||
|
||||
# Apply geometry.
|
||||
if obj.modifiers:
|
||||
ensure_single_user_mesh(obj.data)
|
||||
with context.temp_override(selected_editable_objects=[obj]):
|
||||
bpy.ops.object.convert(target="MESH")
|
||||
|
||||
# Apply scale.
|
||||
if obj.scale != (1, 1, 1):
|
||||
if obj.data.users > 1:
|
||||
bpy.ops.object.make_single_user(
|
||||
object=True, obdata=True, material=False, animation=False, obdata_animation=False
|
||||
)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
|
||||
ensure_single_user_mesh(obj.data)
|
||||
is_negative = obj.matrix_world.is_negative
|
||||
with context.temp_override(selected_editable_objects=[obj]):
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
|
||||
# object.transform_apply is losing normals.
|
||||
if is_negative:
|
||||
for polygon in obj.data.polygons:
|
||||
polygon.flip()
|
||||
|
||||
if tool.Geometry.mesh_has_loose_geometry(obj.data):
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
@@ -252,6 +304,21 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
else:
|
||||
|
||||
def is_representation_supported() -> bool:
|
||||
# We don't support much topological representations
|
||||
# and need to prevent assigning IfcShapeRepresentations to structural items.
|
||||
if is_structural:
|
||||
return False
|
||||
data = obj.data
|
||||
# Is empty mesh.
|
||||
if isinstance(data, bpy.types.Mesh) and not data.vertices:
|
||||
return False
|
||||
# Is empty curve.
|
||||
if isinstance(data, bpy.types.Curve) and not data.splines:
|
||||
return False
|
||||
return True
|
||||
|
||||
element = core.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
@@ -259,12 +326,27 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj=obj,
|
||||
ifc_class=ifc_class,
|
||||
predefined_type=predefined_type,
|
||||
should_add_representation=self.should_add_representation,
|
||||
should_add_representation=self.should_add_representation and is_representation_supported(),
|
||||
context=ifc_context,
|
||||
ifc_representation_class=self.ifc_representation_class,
|
||||
)
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
if representation:
|
||||
tool.Geometry.reload_representation(obj)
|
||||
elif obj.data is not None:
|
||||
new_obj = tool.Geometry.recreate_object_with_data(obj, None)
|
||||
|
||||
context.view_layer.objects.active = active_object
|
||||
# TODO: reload representation might lead to the object being replaced by object of the other type.
|
||||
# We probably should track it somehow and keep the original selection.
|
||||
|
||||
# Validation selection.
|
||||
new_selected_objects = list(filter(tool.Blender.is_valid_data_block, current_selection[2]))
|
||||
active_object = current_selection[1]
|
||||
if active_object and not tool.Blender.is_valid_data_block(active_object):
|
||||
active_object = None
|
||||
current_selection = (current_selection[0], active_object, new_selected_objects)
|
||||
|
||||
tool.Blender.set_objects_selection(*current_selection)
|
||||
|
||||
|
||||
class UnlinkObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -360,6 +442,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
is_specific_tool: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
ifc_product: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
ifc_class: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
skip_dialog: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
|
||||
@@ -380,6 +463,8 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.ifc_product = self.ifc_product
|
||||
if self.ifc_class:
|
||||
props.ifc_class = self.ifc_class
|
||||
if self.skip_dialog:
|
||||
return self.execute(context)
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -190,6 +190,7 @@ class BIMRootProperties(PropertyGroup):
|
||||
|
||||
if TYPE_CHECKING:
|
||||
contexts: str
|
||||
name: str
|
||||
description: str
|
||||
ifc_product: str
|
||||
ifc_class: str
|
||||
|
||||
@@ -40,6 +40,7 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Literal, get_args
|
||||
from typing_extensions import assert_never
|
||||
|
||||
|
||||
class AddFilterGroup(Operator):
|
||||
@@ -144,6 +145,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
|
||||
class Search(Operator):
|
||||
bl_idname = "bim.search"
|
||||
bl_label = "Search"
|
||||
bl_description = "Search IFC elements by the provided query and add them to the current selection."
|
||||
|
||||
PropertyGroupType = Literal["CsvProperties", "BIMSearchProperties"]
|
||||
property_group: bpy.props.EnumProperty(
|
||||
@@ -159,17 +161,18 @@ class Search(Operator):
|
||||
elif self.property_group == "BIMSearchProperties":
|
||||
props = tool.Search.get_search_props()
|
||||
else:
|
||||
raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.")
|
||||
assert_never(self.property_group)
|
||||
|
||||
results = ifcopenshell.util.selector.filter_elements(
|
||||
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
|
||||
)
|
||||
|
||||
total_selected = 0
|
||||
for element in results:
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
obj.select_set(True)
|
||||
self.report({"INFO"}, f"{len(results)} Results")
|
||||
objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)]
|
||||
for obj in objs:
|
||||
tool.Blender.set_object_selection(obj)
|
||||
if objs:
|
||||
tool.Blender.set_active_object(objs[0])
|
||||
self.report({"INFO"}, f"{len(results)} Results.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -659,6 +659,7 @@ class DisableEditingWorkCalendar(bpy.types.Operator):
|
||||
class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_csv"
|
||||
bl_label = "Import CSV"
|
||||
bl_description = "Import work schedule from the provided .csv file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".csv"
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
@@ -686,6 +687,7 @@ class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
class ImportP6(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_p6"
|
||||
bl_label = "Import P6"
|
||||
bl_description = "Import provided .xml P6 file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".xml"
|
||||
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
|
||||
@@ -714,6 +716,7 @@ class ImportP6(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
class ImportP6XER(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_p6xer"
|
||||
bl_label = "Import P6 XER"
|
||||
bl_description = "Import provided .xer P6 file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".xer"
|
||||
filter_glob: bpy.props.StringProperty(default="*.xer", options={"HIDDEN"})
|
||||
@@ -742,6 +745,7 @@ class ImportP6XER(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
class ImportPP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_pp"
|
||||
bl_label = "Import Powerproject .pp"
|
||||
bl_description = "Import provided .pp file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".pp"
|
||||
filter_glob: bpy.props.StringProperty(default="*.pp", options={"HIDDEN"})
|
||||
@@ -770,6 +774,7 @@ class ImportPP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
bl_idname = "bim.import_msp"
|
||||
bl_label = "Import MSP"
|
||||
bl_description = "Import provided .xml MSP file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".xml"
|
||||
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
|
||||
@@ -798,6 +803,7 @@ class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||
class ExportMSP(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_msp"
|
||||
bl_label = "Export MSP"
|
||||
bl_description = "Export work schedule as .xml MSP file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".xml"
|
||||
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
|
||||
@@ -831,6 +837,7 @@ class ExportMSP(bpy.types.Operator, ExportHelper):
|
||||
class ExportP6(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "bim.export_p6"
|
||||
bl_label = "Export P6"
|
||||
bl_description = "Export work schedule as .xml P6 file."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".xml"
|
||||
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
|
||||
|
||||
@@ -24,11 +24,13 @@ classes = (
|
||||
operator.ContractContainer,
|
||||
operator.CopyToContainer,
|
||||
operator.DeleteContainer,
|
||||
operator.DereferenceFromProvidedStructure,
|
||||
operator.DereferenceStructure,
|
||||
operator.DisableEditingContainer,
|
||||
operator.EnableEditingContainer,
|
||||
operator.ExpandContainer,
|
||||
operator.ImportSpatialDecomposition,
|
||||
operator.ReferenceFromProvidedStructure,
|
||||
operator.ReferenceStructure,
|
||||
operator.RemoveContainer,
|
||||
operator.SelectContainer,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell.util.element
|
||||
from typing import Any
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -79,9 +80,20 @@ class SpatialData:
|
||||
return label
|
||||
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = ifcopenshell.util.element.get_referenced_structures(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
return sorted([f"{r.is_a()}/{r.Name or ''}" for r in results])
|
||||
def references(cls) -> list[dict[str, Any]]:
|
||||
assert (obj := bpy.context.active_object) and (element := tool.Ifc.get_entity(obj))
|
||||
results: list[dict[str, Any]] = []
|
||||
for structure in ifcopenshell.util.element.get_referenced_structures(element):
|
||||
ifc_class = structure.is_a()
|
||||
results.append(
|
||||
{
|
||||
"id": structure.id(),
|
||||
"name": f"{ifc_class}/{structure.Name or ''}",
|
||||
"type": ifc_class,
|
||||
}
|
||||
)
|
||||
results.sort(key=lambda x: x["name"])
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def is_directly_contained(cls):
|
||||
|
||||
@@ -49,6 +49,8 @@ class GridDecorator:
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -73,15 +73,85 @@ class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.dereference_structure(tool.Ifc, tool.Spatial, structure=container, element=element)
|
||||
|
||||
|
||||
class ReferenceFromProvidedStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.reference_from_provided_structure"
|
||||
bl_label = "Reference from Provided Structure"
|
||||
bl_description = "Reference selected objects from the provided structure.\n\n" "ALT + Click to dereference instead."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
structure: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||
dereference: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Blender.get_selected_objects():
|
||||
cls.poll_message_set("No objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.dereference = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
structure = ifc_file.by_id(self.structure)
|
||||
|
||||
objs = tool.Spatial.get_selected_objects_without_containers()
|
||||
if not objs:
|
||||
self.report({"INFO"}, "No non-spatial objects are selected.")
|
||||
return
|
||||
|
||||
elements = [e for o in objs if (e := tool.Ifc.get_entity(o))]
|
||||
for element in elements:
|
||||
if self.dereference:
|
||||
core.dereference_structure(tool.Ifc, tool.Spatial, structure=structure, element=element)
|
||||
else:
|
||||
core.reference_structure(tool.Ifc, tool.Spatial, structure=structure, element=element)
|
||||
|
||||
msg = "dereferenced" if self.dereference else "referenced"
|
||||
self.report({"INFO"}, f"{len(elements)} elements {msg} from the structure.")
|
||||
|
||||
|
||||
class DereferenceFromProvidedStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.dereference_from_provided_structure"
|
||||
bl_label = "Dereference from Provided Structure"
|
||||
bl_description = "Dereference selected objects from the provided structure."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
structure: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Blender.get_selected_objects():
|
||||
cls.poll_message_set("No objects selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
structure = ifc_file.by_id(self.structure)
|
||||
objs = tool.Spatial.get_selected_objects_without_containers()
|
||||
if not objs:
|
||||
self.report({"INFO"}, "No non-spatial objects are selected.")
|
||||
return
|
||||
|
||||
elements = [e for o in objs if (e := tool.Ifc.get_entity(o))]
|
||||
for element in elements:
|
||||
core.dereference_structure(tool.Ifc, tool.Spatial, structure=structure, element=element)
|
||||
|
||||
self.report({"INFO"}, f"{len(elements)} elements dereferenced from the structure.")
|
||||
|
||||
|
||||
class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_container"
|
||||
bl_label = "Assign Container"
|
||||
bl_description = "\n".join(
|
||||
(
|
||||
"Assign the selected objects to the selected container.",
|
||||
"This will move objects to the container collection in the outliner.",
|
||||
"ALT + Click to ensure objects are only linked in the container collection",
|
||||
)
|
||||
bl_description = (
|
||||
"Assign the selected objects to the container selected in Spatial Manager.\n\n"
|
||||
"All elements-parts of an aggregate will be skipped.\n"
|
||||
"To assign a container, they should be unassigned from an aggregate first.\n\n"
|
||||
"This will also move objects to the container collection in the outliner.\n"
|
||||
"ALT + Click to ensure objects are only linked in the container collection"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||
@@ -103,12 +173,33 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pass
|
||||
else:
|
||||
return
|
||||
for element_obj in tool.Blender.get_selected_objects():
|
||||
|
||||
objs: list[bpy.types.Object] = []
|
||||
# In IFC element can be either contained of aggregated,
|
||||
# tehrefore we skip aggregated elements here to prevent confusion.
|
||||
# Can't handle it in `poll` since user might just select bunch of elements
|
||||
# and try to assign a container to them
|
||||
# and excluding aggregates because of the `poll` failing might get awkward.
|
||||
skipped_aggregates = 0
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not (element := tool.Ifc.get_entity(obj)):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_aggregate(element):
|
||||
skipped_aggregates += 1
|
||||
continue
|
||||
objs.append(obj)
|
||||
|
||||
for element_obj in objs:
|
||||
if self.remove_from_other_containers:
|
||||
for col in element_obj.users_collection[:]:
|
||||
col.objects.unlink(element_obj)
|
||||
core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj)
|
||||
|
||||
aggregates_msg = ""
|
||||
if skipped_aggregates:
|
||||
aggregates_msg = f" {skipped_aggregates} aggregated elements skipped."
|
||||
self.report({"INFO"}, f"{len(objs)} elements assigned.{aggregates_msg}")
|
||||
|
||||
|
||||
class EnableEditingContainer(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_container"
|
||||
|
||||
@@ -21,7 +21,7 @@ import bpy
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.module.spatial.data import SpatialData, SpatialDecompositionData
|
||||
import bonsai.tool as tool
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, cast, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.spatial.prop import BIMSpatialDecompositionProperties, BIMContainer, Element
|
||||
@@ -56,6 +56,8 @@ class BIM_PT_spatial(Panel):
|
||||
row.operator("bim.assign_container", icon="CHECKMARK", text="")
|
||||
row.operator("bim.disable_editing_container", icon="CANCEL", text="")
|
||||
|
||||
# TODO: deprecate as it's very hard to discover
|
||||
# containers are not even selectable by default.
|
||||
if SpatialData.data["selected_containers"]:
|
||||
row = self.layout.row()
|
||||
row.label(text=f"{len(SpatialData.data['selected_containers'])} Selected Containers")
|
||||
@@ -86,14 +88,16 @@ class BIM_PT_spatial(Panel):
|
||||
row.label(text="No Spatial Container")
|
||||
row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="")
|
||||
|
||||
if references := SpatialData.data["references"]:
|
||||
if references := cast(list[dict[str, Any]], SpatialData.data["references"]):
|
||||
self.layout.label(text=f"{len(references)} References:")
|
||||
else:
|
||||
self.layout.label(text="No References Found")
|
||||
|
||||
for reference in references:
|
||||
row = self.layout.row()
|
||||
row.label(text=reference, icon="LINKED")
|
||||
row.label(text=reference["name"], icon="LINKED")
|
||||
op = row.operator("bim.dereference_from_provided_structure", icon="X", text="")
|
||||
op.structure = reference["id"]
|
||||
|
||||
|
||||
class BIM_PT_spatial_decomposition(Panel):
|
||||
@@ -169,7 +173,14 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
if not self.props.active_container:
|
||||
return
|
||||
|
||||
if not self.props.total_elements:
|
||||
container_has_elements = bool(self.props.total_elements)
|
||||
if container_has_elements:
|
||||
row = self.layout.row()
|
||||
row.label(
|
||||
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
|
||||
icon="FILE_3D",
|
||||
)
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{self.props.active_container.ifc_class} > No Elements", icon="FILE_3D")
|
||||
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
|
||||
@@ -177,37 +188,44 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "element_mode", text="", icon="FILEBROWSER")
|
||||
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(
|
||||
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
|
||||
icon="FILE_3D",
|
||||
)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
op = row.operator("bim.set_element_visibility", icon="FULLSCREEN_EXIT", text="Isolate")
|
||||
col = row.column()
|
||||
row_ = col.row(align=True)
|
||||
row_.enabled = container_has_elements
|
||||
op = row_.operator("bim.set_element_visibility", icon="FULLSCREEN_EXIT", text="")
|
||||
op.mode = "ISOLATE"
|
||||
op.container = ifc_definition_id
|
||||
|
||||
op = row.operator("bim.set_element_visibility", icon="HIDE_OFF", text="")
|
||||
op = row_.operator("bim.set_element_visibility", icon="HIDE_OFF", text="")
|
||||
op.mode = "SHOW"
|
||||
op.container = ifc_definition_id
|
||||
|
||||
op = row.operator("bim.set_element_visibility", icon="HIDE_ON", text="")
|
||||
op = row_.operator("bim.set_element_visibility", icon="HIDE_ON", text="")
|
||||
op.mode = "HIDE"
|
||||
op.container = ifc_definition_id
|
||||
|
||||
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
|
||||
op = row.operator("bim.select_decomposed_element", icon="OBJECT_DATA", text="")
|
||||
col = row.column()
|
||||
row_ = col.row(align=True)
|
||||
row_.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
|
||||
row_.operator("bim.reference_from_provided_structure", icon="LINKED", text="").structure = ifc_definition_id
|
||||
|
||||
col = row.column()
|
||||
row_ = col.row(align=True)
|
||||
row_.enabled = container_has_elements
|
||||
op = row_.operator("bim.select_decomposed_element", icon="OBJECT_DATA", text="")
|
||||
if active_element := self.props.active_element:
|
||||
op.element = active_element.ifc_definition_id
|
||||
else:
|
||||
op.element = 0
|
||||
op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op = row_.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.container = ifc_definition_id
|
||||
|
||||
if not container_has_elements:
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "element_mode", text="", icon="FILEBROWSER")
|
||||
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
|
||||
|
||||
@@ -21,6 +21,7 @@ import numpy as np
|
||||
import bpy
|
||||
import gpu
|
||||
import blf
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from typing import Iterable, Union
|
||||
@@ -86,6 +87,8 @@ class LoadsDecorator:
|
||||
shader = info["shader"]
|
||||
args = info["args"]
|
||||
indices = info["indices"]
|
||||
if not tool.Blender.validate_shader_batch_data(args["coord"], indices):
|
||||
continue
|
||||
batch = batch_for_shader(shader, "TRIS", args, indices=indices)
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
shader.bind()
|
||||
|
||||
@@ -303,6 +303,7 @@ class BrowseExternalStyle(bpy.types.Operator, ImportHelper):
|
||||
|
||||
def get_data_blocks(self, context):
|
||||
l = [("", "", "")]
|
||||
BrowseExternalStyle.data_block_items = l
|
||||
if self.data_block_type != "0" and os.path.exists(self.filepath) and self.filepath.endswith(".blend"):
|
||||
with bpy.data.libraries.load(self.filepath) as (data_from, data_to):
|
||||
objects = getattr(data_from, self.data_block_type)
|
||||
|
||||
@@ -73,6 +73,8 @@ class SystemDecorator:
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||||
return
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
|
||||
@@ -118,23 +118,24 @@ class BooleansData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
# Only called when some object is active.
|
||||
cls.data = {}
|
||||
cls.data["total_booleans"] = cls.booleans()
|
||||
cls.data["manual_booleans"] = cls.manual_booleans()
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def booleans(cls):
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
obj = props.representation_obj or bpy.context.active_object
|
||||
def booleans(cls) -> list[ifcopenshell.entity_instance]:
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
if not (representation := tool.Geometry.get_active_representation(obj)):
|
||||
return []
|
||||
return tool.Model.get_booleans(representation=representation)
|
||||
|
||||
@classmethod
|
||||
def manual_booleans(cls):
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
obj = props.representation_obj or bpy.context.active_object
|
||||
def manual_booleans(cls) -> list[ifcopenshell.entity_instance]:
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
if not (representation := tool.Geometry.get_active_representation(obj)):
|
||||
return []
|
||||
return tool.Model.get_manual_booleans(tool.Ifc.get_entity(obj), representation=representation)
|
||||
|
||||
@@ -32,14 +32,16 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Apply Opening"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Apply an Opening object on an Element. "
|
||||
"The Element and the Opening to be applied should be selected. The order of selection is not important"
|
||||
"Apply opening objects to an Element.\n\n"
|
||||
"The Element and the openings to be applied should be selected. The order of selection is not important.\n"
|
||||
"Opening can be just a Blender mesh object."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if len(context.selected_objects) < 2:
|
||||
cls.poll_message_set("Select openings and a target element")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -52,9 +52,8 @@ class BIM_PT_voids(Panel):
|
||||
if not VoidsData.is_loaded:
|
||||
VoidsData.load()
|
||||
|
||||
if len(context.selected_objects) >= 2:
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator("bim.add_opening", icon="ADD", text="Add Opening")
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator("bim.add_opening", icon="ADD", text="Add Opening")
|
||||
|
||||
if VoidsData.data["active_opening"]:
|
||||
row = self.layout.row()
|
||||
|
||||
@@ -620,7 +620,14 @@ class BIMObjectProperties(PropertyGroup):
|
||||
)
|
||||
cartesian_point_offset: StringProperty(name="Cartesian Point Offset")
|
||||
is_reassigning_class: BoolProperty(name="Is Reassigning Class")
|
||||
is_renaming: BoolProperty(name="Is Renaming", default=False)
|
||||
is_renaming: BoolProperty(
|
||||
name="Is Renaming",
|
||||
description=(
|
||||
"Flag to ensure object name callback wouldn't write new name to IFC. "
|
||||
"Automatically reset to `False` after the next callback."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
location_checksum: StringProperty(name="Location Checksum")
|
||||
rotation_checksum: StringProperty(name="Rotation Checksum")
|
||||
|
||||
|
||||
@@ -247,6 +247,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
default=True,
|
||||
description="If disabled, the toolbar will only load when an IFC model is active",
|
||||
)
|
||||
should_use_snap: BoolProperty(
|
||||
name="Enable Snapping on Startup",
|
||||
default=True,
|
||||
description="If enabled, snapping will be enabled on new sessions.\nIt is recommended to keep this `Enabled`",
|
||||
)
|
||||
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
|
||||
tmp_dir: StringProperty(
|
||||
name="Temporary Directory",
|
||||
@@ -349,13 +354,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
row.operator("bim.file_associate", icon="LOCKVIEW_ON")
|
||||
row.operator("bim.file_unassociate", icon="LOCKVIEW_OFF")
|
||||
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Interface", self.draw_interface_settings)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Command Paths", self.draw_commands)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Misc.", self.draw_misc_settings)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Model", self.draw_model_settings)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Colors", self.draw_decorator_colors)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Directories", self.draw_directories)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Drawing", self.draw_drawing_settings)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Openings", self.draw_openings_settings)
|
||||
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Other", self.draw_other_settings)
|
||||
|
||||
def draw_commands(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "svg2pdf_command")
|
||||
@@ -365,16 +370,14 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
layout.prop(self, "pdf_command")
|
||||
layout.prop(self, "spreadsheet_command")
|
||||
|
||||
def draw_misc_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
def draw_interface_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "should_hide_empty_props")
|
||||
layout.prop(self, "should_setup_workspace")
|
||||
layout.prop(self, "activate_workspace")
|
||||
layout.prop(self, "should_setup_toolbar")
|
||||
layout.prop(self, "should_use_snap")
|
||||
layout.prop(self, "should_play_chaching_sound")
|
||||
layout.prop(self, "spatial_elements_unselectable")
|
||||
props = tool.Project.get_project_props()
|
||||
layout.prop(props, "should_disable_undo_on_save")
|
||||
layout.prop(props, "should_stream")
|
||||
|
||||
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
props = tool.Model.get_model_props()
|
||||
@@ -426,8 +429,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
layout.row().prop(self, "decorator_color_error")
|
||||
layout.row().prop(self, "decorator_color_background")
|
||||
|
||||
def draw_openings_settings(self, layout, context):
|
||||
def draw_other_settings(self, layout, context):
|
||||
layout.prop(self, "opening_focus_opacity")
|
||||
props = tool.Project.get_project_props()
|
||||
layout.prop(props, "should_disable_undo_on_save")
|
||||
layout.prop(props, "should_stream")
|
||||
|
||||
|
||||
# Scene panel groups
|
||||
@@ -1262,8 +1268,8 @@ class BIM_PT_decorators_overlay(Panel):
|
||||
overlay = view.overlay
|
||||
|
||||
georeference_props = tool.Georeference.get_georeference_props()
|
||||
aggregate_props = bpy.context.scene.BIMAggregateProperties
|
||||
nest_props = bpy.context.scene.BIMNestProperties
|
||||
aggregate_props = tool.Aggregate.get_aggregate_props()
|
||||
nest_props = tool.Nest.get_nest_props()
|
||||
model_props = tool.Model.get_model_props()
|
||||
display_all = overlay.show_overlays
|
||||
|
||||
|
||||
@@ -16,11 +16,22 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.core.tool as tool
|
||||
from typing import Literal, Iterable
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional, Literal, Iterable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
|
||||
from mathutils import Vector
|
||||
from bonsai.bim.module.model.wall import DumbWallJoiner
|
||||
|
||||
|
||||
def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None:
|
||||
def unjoin_walls(
|
||||
ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner: DumbWallJoiner, model: tool.Model
|
||||
) -> None:
|
||||
"""Unjoin selected walls."""
|
||||
for obj in blender.get_selected_objects():
|
||||
if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2":
|
||||
continue
|
||||
@@ -31,8 +42,14 @@ def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry,
|
||||
|
||||
|
||||
def extend_walls(
|
||||
ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model, target
|
||||
ifc: tool.Ifc,
|
||||
blender: tool.Blender,
|
||||
geometry: tool.Geometry,
|
||||
joiner: DumbWallJoiner,
|
||||
model: tool.Model,
|
||||
target: Vector,
|
||||
) -> None:
|
||||
"""Extend selected walls to the target."""
|
||||
for obj in blender.get_selected_objects():
|
||||
if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2":
|
||||
continue
|
||||
@@ -44,7 +61,7 @@ def join_walls_LV(
|
||||
ifc: tool.Ifc,
|
||||
blender: tool.Blender,
|
||||
geometry: tool.Geometry,
|
||||
joiner,
|
||||
joiner: DumbWallJoiner,
|
||||
model: tool.Model,
|
||||
join_type: Literal["L", "V"] = "L",
|
||||
) -> None:
|
||||
@@ -66,7 +83,11 @@ def join_walls_LV(
|
||||
|
||||
|
||||
def extend_wall_to_slab(
|
||||
ifc: tool.Ifc, geometry: tool.Geometry, model: tool.Model, slab_obj, wall_objs: Iterable
|
||||
ifc: tool.Ifc,
|
||||
geometry: tool.Geometry,
|
||||
model: tool.Model,
|
||||
slab_obj: bpy.types.Object,
|
||||
wall_objs: list[bpy.types.Object],
|
||||
) -> None:
|
||||
if not (clip := model.get_slab_clipping_bmesh(slab_obj)):
|
||||
return # Nothing to clip?
|
||||
@@ -80,7 +101,9 @@ def extend_wall_to_slab(
|
||||
model.reload_body_representation(wall_objs)
|
||||
|
||||
|
||||
def join_walls_TZ(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None:
|
||||
def join_walls_TZ(
|
||||
ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner: DumbWallJoiner, model: tool.Model
|
||||
) -> None:
|
||||
selected_objs = [
|
||||
o
|
||||
for o in blender.get_selected_objects()
|
||||
|
||||
@@ -25,15 +25,21 @@ if TYPE_CHECKING:
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def enable_editing_nest(nest, obj=None):
|
||||
def enable_editing_nest(nest: tool.Nest, obj: bpy.types.Object) -> None:
|
||||
nest.enable_editing(obj)
|
||||
|
||||
|
||||
def disable_editing_nest(nest, obj=None):
|
||||
def disable_editing_nest(nest: tool.Nest, obj: bpy.types.Object) -> None:
|
||||
nest.disable_editing(obj)
|
||||
|
||||
|
||||
def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
|
||||
def assign_object(
|
||||
ifc: tool.Ifc,
|
||||
nest: tool.Nest,
|
||||
collector: tool.Collector,
|
||||
relating_obj: bpy.types.Object,
|
||||
related_obj: bpy.types.Object,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not nest.can_nest(relating_obj, related_obj):
|
||||
return
|
||||
rel = ifc.run(
|
||||
@@ -47,14 +53,22 @@ def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
|
||||
return rel
|
||||
|
||||
|
||||
def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
|
||||
def unassign_object(
|
||||
ifc: tool.Ifc,
|
||||
nest: tool.Nest,
|
||||
collector: tool.Collector,
|
||||
relating_obj: bpy.types.Object,
|
||||
related_obj: bpy.types.Object,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
related_element = ifc.get_entity(related_obj)
|
||||
assert related_element
|
||||
container = nest.get_container(related_element)
|
||||
# TODO: this branch is never used?
|
||||
if not relating_obj:
|
||||
relating_element = nest.get_relating_object(related_element)
|
||||
if related_element:
|
||||
relating_obj = ifc.get_object(relating_element)
|
||||
if relating_obj:
|
||||
else:
|
||||
ifc.run("nest.unassign_object", related_objects=[related_element])
|
||||
if container:
|
||||
ifc.run("spatial.assign_container", products=[related_element], relating_structure=container)
|
||||
@@ -62,21 +76,28 @@ def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
|
||||
collector.assign(related_obj)
|
||||
|
||||
|
||||
def add_part_to_object(ifc, nest, collector, blender, obj, part_class, part_name=None):
|
||||
def add_part_to_object(
|
||||
ifc: tool.Ifc,
|
||||
nest: tool.Nest,
|
||||
collector: tool.Collector,
|
||||
blender: tool.Blender,
|
||||
obj: bpy.types.Object,
|
||||
part_class: str,
|
||||
part_name: str,
|
||||
) -> None:
|
||||
part_obj = blender.create_ifc_object(ifc_class=part_class, name=part_name)
|
||||
assign_object(ifc, nest, collector, relating_obj=obj, related_obj=part_obj)
|
||||
blender.set_active_object(obj)
|
||||
|
||||
|
||||
def enable_nest_mode(
|
||||
nest: tool.Nest,
|
||||
obj: bpy.types.Object,
|
||||
):
|
||||
) -> None:
|
||||
if nest.get_nest_mode():
|
||||
disable_nest_mode(nest)
|
||||
|
||||
nest.enable_nest_mode(obj)
|
||||
|
||||
|
||||
def disable_nest_mode(nest: tool.Nest):
|
||||
def disable_nest_mode(nest: tool.Nest) -> None:
|
||||
nest.disable_nest_mode()
|
||||
|
||||
@@ -558,7 +558,7 @@ class Misc:
|
||||
def run_root_copy_class(cls, obj=None): pass
|
||||
def scale_object_to_height(cls, obj, height): pass
|
||||
def set_object_origin_to_bottom(cls, obj): pass
|
||||
def split_objects_with_cutter(cls, objs, cutter): pass
|
||||
def boolean_objects_with_cutter(cls, objs, cutter): pass
|
||||
|
||||
|
||||
@interface
|
||||
|
||||
@@ -16,15 +16,27 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
from typing import Union
|
||||
from typing import Union, TYPE_CHECKING, Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.aggregate.prop import BIMAggregateProperties, BIMObjectAggregateProperties
|
||||
|
||||
|
||||
class Aggregate(bonsai.core.tool.Aggregate):
|
||||
@classmethod
|
||||
def get_aggregate_props(cls) -> BIMAggregateProperties:
|
||||
return bpy.context.scene.BIMAggregateProperties
|
||||
|
||||
@classmethod
|
||||
def get_object_aggregate_props(cls, obj: bpy.types.Object) -> BIMObjectAggregateProperties:
|
||||
return obj.BIMObjectAggregateProperties
|
||||
|
||||
@classmethod
|
||||
def can_aggregate(cls, relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
|
||||
relating_object = tool.Ifc.get_entity(relating_obj)
|
||||
@@ -56,11 +68,13 @@ class Aggregate(bonsai.core.tool.Aggregate):
|
||||
|
||||
@classmethod
|
||||
def disable_editing(cls, obj: bpy.types.Object) -> None:
|
||||
obj.BIMObjectAggregateProperties.is_editing = False
|
||||
props = cls.get_object_aggregate_props(obj)
|
||||
props.is_editing = False
|
||||
|
||||
@classmethod
|
||||
def enable_editing(cls, obj: bpy.types.Object) -> None:
|
||||
obj.BIMObjectAggregateProperties.is_editing = True
|
||||
props = cls.get_object_aggregate_props(obj)
|
||||
props.is_editing = True
|
||||
|
||||
@classmethod
|
||||
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
@@ -86,13 +100,14 @@ class Aggregate(bonsai.core.tool.Aggregate):
|
||||
return parts
|
||||
|
||||
@classmethod
|
||||
def get_aggregate_mode(cls):
|
||||
return bpy.context.scene.BIMAggregateProperties.in_aggregate_mode
|
||||
def get_aggregate_mode(cls) -> bool:
|
||||
props = cls.get_aggregate_props()
|
||||
return props.in_aggregate_mode
|
||||
|
||||
@classmethod
|
||||
def enable_aggregate_mode(cls, active_object: bpy.types.Object):
|
||||
def enable_aggregate_mode(cls, active_object: bpy.types.Object) -> set[Literal["FINISHED"]]:
|
||||
context = bpy.context
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = cls.get_aggregate_props()
|
||||
|
||||
element = tool.Ifc.get_entity(active_object)
|
||||
if not element:
|
||||
@@ -131,7 +146,7 @@ class Aggregate(bonsai.core.tool.Aggregate):
|
||||
@classmethod
|
||||
def disable_aggregate_mode(cls):
|
||||
context = bpy.context
|
||||
props = context.scene.BIMAggregateProperties
|
||||
props = cls.get_aggregate_props()
|
||||
for obj_prop in props.not_editing_objects:
|
||||
obj = obj_prop.obj
|
||||
if not obj:
|
||||
|
||||
@@ -43,6 +43,7 @@ from typing_extensions import assert_never
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import BIMProperties, BIMObjectProperties
|
||||
from bonsai.bim.module.attribute.prop import BIMAttributeProperties
|
||||
from bonsai.bim.module.csv.prop import CsvProperties
|
||||
from bonsai.bim.module.diff.prop import DiffProperties
|
||||
|
||||
@@ -113,11 +114,13 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
@classmethod
|
||||
def set_active_object(cls, obj: bpy.types.Object) -> None:
|
||||
"""Set active object and select it."""
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def clear_active_object(cls) -> None:
|
||||
"""Clear active object, object is not unselected."""
|
||||
bpy.context.view_layer.objects.active = None
|
||||
|
||||
@classmethod
|
||||
@@ -655,6 +658,12 @@ class Blender(bonsai.core.tool.Blender):
|
||||
if active_object:
|
||||
active_object.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def clear_objects_selection(cls) -> None:
|
||||
"""Clear objects selection and active object."""
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
cls.clear_active_object()
|
||||
|
||||
@classmethod
|
||||
def get_enum_safe(cls, props: bpy.types.PropertyGroup, prop_name: str) -> Union[str, None]:
|
||||
"""method created for readibility and to avoid console warnings like
|
||||
@@ -1629,6 +1638,10 @@ class Blender(bonsai.core.tool.Blender):
|
||||
def get_object_bim_props(cls, obj: bpy.types.Object) -> BIMObjectProperties:
|
||||
return obj.BIMObjectProperties
|
||||
|
||||
@classmethod
|
||||
def get_object_attribute_props(cls, obj: bpy.types.Object) -> BIMAttributeProperties:
|
||||
return obj.BIMAttributeProperties
|
||||
|
||||
@classmethod
|
||||
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
@@ -1668,3 +1681,21 @@ class Blender(bonsai.core.tool.Blender):
|
||||
unit_scale = 0.3048
|
||||
|
||||
return unit_scale
|
||||
|
||||
@classmethod
|
||||
def validate_shader_batch_data(cls, pos: Any, indices: Optional[Any]) -> bool:
|
||||
"""Validate shader batch data.
|
||||
|
||||
If method returns ``False``, then drawing for this batch should be skipped.
|
||||
Should be used always before running ``batch.draw(shader)``
|
||||
|
||||
Important because in Blender 4.4.0 on Mac passing an empty list
|
||||
as ``indices`` is causing a crash.
|
||||
|
||||
See https://projects.blender.org/blender/blender/issues/136831
|
||||
"""
|
||||
# Checking `pos` is not critical but we keep it
|
||||
# to ensure batch data is always validated to avoid crashes.
|
||||
if len(pos) == 0 or (indices is not None and len(indices) == 0):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -456,7 +456,7 @@ class Brick(bonsai.core.tool.Brick):
|
||||
|
||||
@classmethod
|
||||
def add_namespace(cls, alias: str, uri: str) -> None:
|
||||
assert BrickStore.graph
|
||||
assert BrickStore.graph is not None
|
||||
BrickStore.graph.bind(alias, Namespace(uri))
|
||||
BrickStore.load_namespaces()
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class Debug(bonsai.core.tool.Debug):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def debug_bmesh(cls, bm: bpy.types.BMesh, name: str = "Debug") -> bpy.types.Object:
|
||||
def debug_bmesh(cls, bm: bmesh.types.BMesh, name: str = "Debug") -> bpy.types.Object:
|
||||
mesh = bpy.data.meshes.new("Debug")
|
||||
bm.to_mesh(mesh)
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
|
||||
@@ -202,6 +202,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
|
||||
@classmethod
|
||||
def delete_ifc_item(cls, obj: bpy.types.Object) -> None:
|
||||
"""Delete IfcRepresentationItem's Object."""
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
if len(props.item_objs) == 1:
|
||||
return
|
||||
@@ -212,7 +213,9 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
item = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id)
|
||||
cls.remove_representation_item(item)
|
||||
rep_obj = props.representation_obj
|
||||
assert (rep_obj := props.representation_obj) and (rep_element := tool.Ifc.get_entity(rep_obj))
|
||||
cls.remove_representation_item(item, rep_element)
|
||||
cls.reload_representation(props.representation_obj)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
@@ -1344,12 +1347,19 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def remove_representation_item(cls, representation_item: ifcopenshell.entity_instance) -> None:
|
||||
def remove_representation_item(
|
||||
cls, representation_item: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
"""Remove IfcRepresentationItem.
|
||||
|
||||
:param representation_item: item to remove.
|
||||
:param element: item's element. Is used to unmark manual booleans.
|
||||
"""
|
||||
# NOTE: we assume it's not the last representation item
|
||||
# otherwise we probably would need to remove representation too
|
||||
# NOTE: a lot of shared code with `geometry.remove_representation`
|
||||
ifc_file = tool.Ifc.get()
|
||||
shape_aspects = []
|
||||
shape_aspects: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
consider_inverses = []
|
||||
styled_item, colour, texture, layer = None, None, None, None
|
||||
@@ -1366,7 +1376,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
[consider_inverses.append(texture := t) for t in getattr(representation_item, "HasTextures", [])]
|
||||
|
||||
representation = None
|
||||
boolean_results_to_remove = set()
|
||||
boolean_results_to_remove: set[ifcopenshell.entity_instance] = set()
|
||||
for inverse in ifc_file.get_inverse(representation_item):
|
||||
if inverse.is_a("IfcShapeRepresentation"):
|
||||
if inverse.OfShapeAspect:
|
||||
@@ -1409,11 +1419,9 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
also_consider = list(consider_inverses)
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
|
||||
|
||||
props = tool.Geometry.get_geometry_props()
|
||||
rep_element = tool.Ifc.get_entity(props.representation_obj)
|
||||
tool.Model.unmark_manual_booleans(rep_element, [b.id() for b in boolean_results_to_remove])
|
||||
tool.Model.unmark_manual_booleans(element, [b.id() for b in boolean_results_to_remove])
|
||||
for boolean_result in boolean_results_to_remove:
|
||||
cls.remove_representation_item(boolean_result)
|
||||
cls.remove_representation_item(boolean_result, element)
|
||||
|
||||
@classmethod
|
||||
def create_shape_aspect(
|
||||
|
||||
@@ -799,6 +799,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
|
||||
# TODO implement non cartesian point vertices.
|
||||
if not point.is_a("IfcCartesianPoint"):
|
||||
print(f"WARNING. Unsupported point type for IfcStructuralPointConnection: {point}.")
|
||||
return
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
@@ -34,6 +34,7 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.tool
|
||||
@@ -61,6 +62,7 @@ if TYPE_CHECKING:
|
||||
BIMWindowProperties,
|
||||
BIMStairProperties,
|
||||
BIMRailingProperties,
|
||||
BIMExternalParametricGeometryProperties,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,6 +95,10 @@ class Model(bonsai.core.tool.Model):
|
||||
def get_array_props(cls, obj: bpy.types.Object) -> BIMArrayProperties:
|
||||
return obj.BIMArrayProperties
|
||||
|
||||
@classmethod
|
||||
def get_epg_props(cls, obj: bpy.types.Object) -> BIMExternalParametricGeometryProperties:
|
||||
return obj.BIMExternalParametricGeometryProperties
|
||||
|
||||
@classmethod
|
||||
def convert_si_to_unit(cls, value: T) -> T:
|
||||
if isinstance(value, (tuple, list)):
|
||||
@@ -687,8 +693,12 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
@classmethod
|
||||
def unmark_manual_booleans(cls, element: ifcopenshell.entity_instance, boolean_ids: list[int]) -> None:
|
||||
# NOTE: we use use boolean_ids instead of boolean entities
|
||||
# so it will be possible to unmark manual booleans after they already was deleted
|
||||
"""Remove boolean ids from ``element``'s 'BBIM_Boolean' pset.
|
||||
|
||||
:param boolean_ids: List of boolean ids to remove.
|
||||
Ids are used instead of entities to make it possible to unmark already removed booleans.
|
||||
Provided ids may not be marked as manual booleans previously.
|
||||
"""
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
|
||||
if not pset:
|
||||
return
|
||||
@@ -1133,6 +1143,27 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
AuthoringData.type_thumbnails[element.id()] = obj.preview.icon_id
|
||||
|
||||
@classmethod
|
||||
def get_selected_ifc_objects(cls) -> list[bpy.types.Object]:
|
||||
return [obj for obj in tool.Blender.get_selected_objects() if tool.Ifc.get_entity(obj)]
|
||||
|
||||
@classmethod
|
||||
def has_selected_ifc_objects(cls) -> bool:
|
||||
return any(tool.Ifc.get_entity(obj) for obj in tool.Blender.get_selected_objects())
|
||||
|
||||
@classmethod
|
||||
def get_selected_mesh_objects(cls) -> list[bpy.types.Object]:
|
||||
objects = tool.Blender.get_selected_objects()
|
||||
return [obj for obj in objects if obj.type == "MESH"]
|
||||
|
||||
@classmethod
|
||||
def get_selected_mesh_ifc_objects(cls) -> list[bpy.types.Object]:
|
||||
return [obj for obj in tool.Model.get_selected_mesh_objects() if tool.Ifc.get_entity(obj)]
|
||||
|
||||
@classmethod
|
||||
def has_selected_mesh_ifc_objects(cls) -> bool:
|
||||
return any(tool.Ifc.get_entity(obj) for obj in tool.Model.get_selected_mesh_objects())
|
||||
|
||||
BBIM_PARAMETRIC_PSETS = (
|
||||
"BBIM_Window",
|
||||
"BBIM_Door",
|
||||
@@ -1570,15 +1601,13 @@ class Model(bonsai.core.tool.Model):
|
||||
return occurrences
|
||||
|
||||
@classmethod
|
||||
def add_body_representation(cls, obj: bpy.types.Object) -> None:
|
||||
def add_representation(cls, obj: bpy.types.Object, context: ifcopenshell.entity_instance) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
assert body
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
representation = ifcopenshell.api.geometry.add_representation(
|
||||
ifc_file,
|
||||
context=body,
|
||||
context=context,
|
||||
blender_object=obj,
|
||||
geometry=mesh,
|
||||
coordinate_offset=tool.Geometry.get_cartesian_point_offset(obj),
|
||||
@@ -1590,7 +1619,14 @@ class Model(bonsai.core.tool.Model):
|
||||
profile_set_usage=None,
|
||||
)
|
||||
assert representation
|
||||
tool.Model.replace_object_ifc_representation(body, obj, representation)
|
||||
tool.Model.replace_object_ifc_representation(context, obj, representation)
|
||||
|
||||
@classmethod
|
||||
def add_body_representation(cls, obj: bpy.types.Object) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
assert body
|
||||
cls.add_representation(obj, body)
|
||||
|
||||
@classmethod
|
||||
def auto_detect_annotation_fill_area(cls, obj: bpy.types.Object, mesh: bpy.types.Mesh) -> dict | None:
|
||||
@@ -2100,7 +2136,7 @@ class Model(bonsai.core.tool.Model):
|
||||
op.material_id = material_id_int
|
||||
|
||||
@classmethod
|
||||
def get_slab_clipping_bmesh(cls, obj: bpy.types.Object) -> bpy.types.BMesh | None:
|
||||
def get_slab_clipping_bmesh(cls, obj: bpy.types.Object) -> bmesh.types.BMesh | None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
bm = bmesh.new()
|
||||
@@ -2132,7 +2168,7 @@ class Model(bonsai.core.tool.Model):
|
||||
return clipping_bm # clipping_bm is in project units
|
||||
|
||||
@classmethod
|
||||
def clip_wall_to_slab(cls, wall: ifcopenshell.entity_instance, clipping_bm: bpy.types.BMesh) -> None:
|
||||
def clip_wall_to_slab(cls, wall: ifcopenshell.entity_instance, clipping_bm: bmesh.types.BMesh) -> None:
|
||||
matrix_i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall.ObjectPlacement))
|
||||
bm = clipping_bm.copy()
|
||||
bmesh.ops.transform(bm, matrix=Matrix(matrix_i.tolist()), verts=bm.verts)
|
||||
@@ -2180,3 +2216,112 @@ class Model(bonsai.core.tool.Model):
|
||||
ifcopenshell.api.geometry.connect_element(
|
||||
tool.Ifc.get(), relating_element=slab, related_element=wall, description="TOP"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_epg_modifier(cls, obj: bpy.types.Object) -> Union[bpy.types.NodesModifier, None]:
|
||||
for m in obj.modifiers:
|
||||
if m.type == "NODES" and m.name.startswith("BBIM_EPG"):
|
||||
assert isinstance(m, bpy.types.NodesModifier)
|
||||
return m
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def setup_external_nodes(
|
||||
cls, modifier: bpy.types.NodesModifier, external_nodes: bpy.types.GeometryNodeTree
|
||||
) -> None:
|
||||
bbim_nodes = modifier.node_group
|
||||
|
||||
if bbim_nodes is not None:
|
||||
# Just assign modifier to existing node group.
|
||||
assert isinstance(bbim_nodes, bpy.types.GeometryNodeTree)
|
||||
group_node = next(n for n in bbim_nodes.nodes if n.type == "GROUP")
|
||||
assert isinstance(group_node, bpy.types.GeometryNodeGroup)
|
||||
group_node.node_tree = external_nodes
|
||||
return
|
||||
|
||||
# Create a new node group.
|
||||
bbim_nodes = bpy.data.node_groups.new(type="GeometryNodeTree", name="BBIM_EPG")
|
||||
modifier.node_group = bbim_nodes
|
||||
|
||||
assert isinstance(bbim_nodes, bpy.types.GeometryNodeTree)
|
||||
bbim_nodes_interface = bbim_nodes.interface
|
||||
assert bbim_nodes_interface
|
||||
|
||||
geometry_socket_2 = bbim_nodes_interface.new_socket(
|
||||
name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry"
|
||||
)
|
||||
geometry_socket_2.attribute_domain = "POINT"
|
||||
|
||||
# Socket Geometry
|
||||
geometry_socket_3 = bbim_nodes_interface.new_socket(
|
||||
name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry"
|
||||
)
|
||||
geometry_socket_3.attribute_domain = "POINT"
|
||||
|
||||
# Socket Socket
|
||||
socket_socket = bbim_nodes_interface.new_socket(name="Socket", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
socket_socket.attribute_domain = "POINT"
|
||||
|
||||
# Initialize bbim_epg nodes.
|
||||
# Node Group Input.
|
||||
group_input_1 = bbim_nodes.nodes.new("NodeGroupInput")
|
||||
assert isinstance(group_input_1, bpy.types.NodeGroupInput)
|
||||
group_input_1.name = "Group Input"
|
||||
# Node Group Output.
|
||||
group_output_1 = bbim_nodes.nodes.new("NodeGroupOutput")
|
||||
assert isinstance(group_output_1, bpy.types.NodeGroupOutput)
|
||||
group_output_1.name = "Group Output"
|
||||
group_output_1.is_active_output = True
|
||||
|
||||
# Node Group.
|
||||
group = bbim_nodes.nodes.new("GeometryNodeGroup")
|
||||
assert isinstance(group, bpy.types.GeometryNodeGroup)
|
||||
group.name = "Group"
|
||||
group.node_tree = external_nodes
|
||||
|
||||
# Set locations
|
||||
group_input_1.location = (-345.0525817871094, 65.80108642578125)
|
||||
group_output_1.location = (200.0, 0.0)
|
||||
group.location = (-83.36784362792969, 80.47976684570312)
|
||||
|
||||
# Set dimensions
|
||||
group_input_1.width, group_input_1.height = 140.0, 100.0
|
||||
group_output_1.width, group_output_1.height = 140.0, 100.0
|
||||
group.width, group.height = 179.41021728515625, 100.0
|
||||
|
||||
# Initialize bbim_epg links.
|
||||
# group.Geometry -> group_output_1.Geometry
|
||||
bbim_nodes.links.new(group.outputs[0], group_output_1.inputs[0])
|
||||
# group_input_1.Geometry -> group.Geometry
|
||||
bbim_nodes.links.new(group_input_1.outputs[0], group.inputs[0])
|
||||
|
||||
@classmethod
|
||||
def setup_parametric_geometry(cls, obj: bpy.types.Object) -> None:
|
||||
props = cls.get_epg_props(obj)
|
||||
external_nodes = props.geo_nodes
|
||||
assert external_nodes
|
||||
|
||||
if not (modifier := cls.get_epg_modifier(obj)):
|
||||
modifier = obj.modifiers.new(type="NODES", name="BBIM_EPG")
|
||||
assert isinstance(modifier, bpy.types.NodesModifier)
|
||||
modifier.show_viewport = True
|
||||
cls.setup_external_nodes(modifier, external_nodes)
|
||||
|
||||
@classmethod
|
||||
def clean_up_parametric_geometry(cls, obj: bpy.types.Object) -> None:
|
||||
modifier = tool.Model.get_epg_modifier(obj)
|
||||
assert modifier
|
||||
node_tree = modifier.node_group
|
||||
assert node_tree
|
||||
bpy.data.node_groups.remove(node_tree)
|
||||
obj.modifiers.clear()
|
||||
|
||||
@classmethod
|
||||
def get_parametric_geometry_inputs(cls, modifier: bpy.types.NodesModifier) -> list[bpy.types.NodeSocket]:
|
||||
node_group = modifier.node_group
|
||||
assert isinstance(node_group, bpy.types.GeometryNodeTree)
|
||||
|
||||
group_node = next(n for n in node_group.nodes if n.type == "GROUP")
|
||||
assert isinstance(group_node, bpy.types.GeometryNodeGroup)
|
||||
|
||||
return [s for s in group_node.inputs if s.type != "GEOMETRY"]
|
||||
|
||||
@@ -16,13 +16,26 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell.util.element
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.nest.prop import BIMNestProperties, BIMObjectNestProperties
|
||||
|
||||
|
||||
class Nest(bonsai.core.tool.Nest):
|
||||
@classmethod
|
||||
def get_nest_props(cls) -> BIMNestProperties:
|
||||
return bpy.context.scene.BIMNestProperties
|
||||
|
||||
@classmethod
|
||||
def get_object_nest_props(cls, obj: bpy.types.Object) -> BIMObjectNestProperties:
|
||||
return obj.BIMObjectNestProperties
|
||||
|
||||
@classmethod
|
||||
def can_nest(cls, relating_obj, related_obj):
|
||||
relating_object = tool.Ifc.get_entity(relating_obj)
|
||||
@@ -34,19 +47,23 @@ class Nest(bonsai.core.tool.Nest):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def disable_editing(cls, obj):
|
||||
obj.BIMObjectNestProperties.is_editing = False
|
||||
def disable_editing(cls, obj: bpy.types.Object) -> None:
|
||||
props = cls.get_object_nest_props(obj)
|
||||
props.is_editing = False
|
||||
|
||||
@classmethod
|
||||
def enable_editing(cls, obj):
|
||||
obj.BIMObjectNestProperties.is_editing = True
|
||||
def enable_editing(cls, obj: bpy.types.Object) -> None:
|
||||
props = cls.get_object_nest_props(obj)
|
||||
props.is_editing = True
|
||||
|
||||
@classmethod
|
||||
def get_container(cls, element):
|
||||
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return ifcopenshell.util.element.get_container(element)
|
||||
|
||||
@classmethod
|
||||
def get_relating_object(cls, related_element):
|
||||
def get_relating_object(
|
||||
cls, related_element: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return ifcopenshell.util.element.get_nest(related_element)
|
||||
|
||||
@classmethod
|
||||
@@ -61,13 +78,14 @@ class Nest(bonsai.core.tool.Nest):
|
||||
return components
|
||||
|
||||
@classmethod
|
||||
def get_nest_mode(cls):
|
||||
return bpy.context.scene.BIMNestProperties.in_nest_mode
|
||||
def get_nest_mode(cls) -> bool:
|
||||
props = cls.get_nest_props()
|
||||
return props.in_nest_mode
|
||||
|
||||
@classmethod
|
||||
def enable_nest_mode(cls, active_object: bpy.types.Object):
|
||||
context = bpy.context
|
||||
props = context.scene.BIMNestProperties
|
||||
props = cls.get_nest_props()
|
||||
|
||||
element = tool.Ifc.get_entity(active_object)
|
||||
if not element:
|
||||
|
||||
@@ -52,15 +52,43 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
else: # Usual object
|
||||
obj = dup.object
|
||||
all_objs.append(obj)
|
||||
return all_objs
|
||||
|
||||
visible_objs = []
|
||||
for obj in all_objs:
|
||||
if obj.type in {"MESH", "EMPTY", "CURVE"} and (
|
||||
obj.visible_in_viewport_get(bpy.context.space_data) or obj.library
|
||||
): # Check for local view and local collections for this viewport and object
|
||||
visible_objs.append(obj)
|
||||
return visible_objs
|
||||
|
||||
@classmethod
|
||||
def get_on_screen_2d_bounding_boxes(cls, context: bpy.types.Context, obj: bpy.types.Object):
|
||||
def get_on_screen_2d_bounding_boxes(
|
||||
cls, context: bpy.types.Context, obj: bpy.types.Object
|
||||
) -> Union[tuple[bpy.types.Object, list[float]], None]:
|
||||
rv3d = context.region_data
|
||||
view_location = rv3d.view_matrix.inverted().translation
|
||||
obj_matrix = obj.matrix_world.copy()
|
||||
bbox = [obj_matrix @ Vector(v) for v in obj.bound_box]
|
||||
|
||||
transposed_bbox = []
|
||||
bbox_2d = []
|
||||
transposed_bbox: list[Vector] = []
|
||||
bbox_2d: list[float] = []
|
||||
|
||||
assert context.region
|
||||
assert isinstance(context.space_data, bpy.types.SpaceView3D)
|
||||
assert context.space_data.region_3d
|
||||
|
||||
# Do not include objects too far from camera view
|
||||
if rv3d.view_perspective == "PERSP":
|
||||
threshold = 200
|
||||
min_distance = float("inf")
|
||||
closest_distance: float = None
|
||||
for point in bbox:
|
||||
distance = (view_location - point).length
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_distance = distance
|
||||
if closest_distance > threshold:
|
||||
return None
|
||||
|
||||
for v in bbox:
|
||||
coord_2d = view3d_utils.location_3d_to_region_2d(context.region, context.space_data.region_3d, v)
|
||||
@@ -68,18 +96,24 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
transposed_bbox.append(coord_2d)
|
||||
|
||||
region = context.region
|
||||
borders = (region.width, region.height)
|
||||
borders = (0, region.width, 0, region.height)
|
||||
for i, axis in enumerate(zip(*transposed_bbox)):
|
||||
axis: tuple[float, ...]
|
||||
min_point = min(axis)
|
||||
max_point = max(axis)
|
||||
if min_point == max_point:
|
||||
min_point = 0
|
||||
if min_point < borders[i] and max_point > 0:
|
||||
bbox_2d.extend([min_point, max_point])
|
||||
else:
|
||||
return (obj, None)
|
||||
bbox_2d.extend([min_point, max_point])
|
||||
|
||||
return (obj, bbox_2d)
|
||||
if len(bbox_2d) == 0:
|
||||
return None
|
||||
# AABB
|
||||
if (
|
||||
bbox_2d[0] <= borders[1]
|
||||
and bbox_2d[1] >= borders[0]
|
||||
and bbox_2d[2] <= borders[3]
|
||||
and bbox_2d[3] >= borders[2]
|
||||
):
|
||||
return (obj, bbox_2d)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float]):
|
||||
@@ -180,9 +214,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
# Makes the snapping point more or less sticky than others
|
||||
# It changes the distance and affects how the snapping point are sorted
|
||||
# We multiply by the increment snap which is based on the viewport zoom
|
||||
snap_threshold = 10 * tool.Snap.get_increment_snap_value(bpy.context)
|
||||
if face:
|
||||
snap_threshold = tool.Snap.get_increment_snap_value(bpy.context)
|
||||
snap_threshold = rv3d.view_distance / 100
|
||||
|
||||
try:
|
||||
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
|
||||
@@ -275,7 +307,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
rv3d = context.region_data
|
||||
mouse_pos = event.mouse_region_x, event.mouse_region_y
|
||||
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
|
||||
snap_threshold = tool.Snap.get_increment_snap_value(bpy.context)
|
||||
snap_threshold = rv3d.view_distance / 100
|
||||
|
||||
try:
|
||||
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
|
||||
@@ -352,7 +384,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
rv3d = context.region_data
|
||||
mouse_pos = event.mouse_region_x, event.mouse_region_y
|
||||
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
|
||||
snap_threshold = tool.Snap.get_increment_snap_value(bpy.context)
|
||||
snap_threshold = rv3d.view_distance / 100
|
||||
|
||||
try:
|
||||
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
|
||||
@@ -386,12 +418,9 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
mouse_pos = event.mouse_region_x, event.mouse_region_y
|
||||
objs_to_raycast = []
|
||||
for obj, bbox_2d in objs_2d_bbox:
|
||||
if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d:
|
||||
if bbox_2d:
|
||||
if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d):
|
||||
if (
|
||||
obj.visible_in_viewport_get(bpy.context.space_data) or obj.library
|
||||
): # Check for local view and local collections for this viewport and object
|
||||
objs_to_raycast.append(obj)
|
||||
objs_to_raycast.append(obj)
|
||||
return objs_to_raycast
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -238,13 +238,13 @@ class Root(bonsai.core.tool.Root):
|
||||
|
||||
@classmethod
|
||||
def is_in_aggregate_mode(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
props = bpy.context.scene.BIMAggregateProperties
|
||||
props = tool.Aggregate.get_aggregate_props()
|
||||
if props.editing_aggregate and props.in_aggregate_mode:
|
||||
return tool.Ifc.get_entity(props.editing_aggregate)
|
||||
|
||||
@classmethod
|
||||
def is_in_nest_mode(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
props = bpy.context.scene.BIMNestProperties
|
||||
props = tool.Nest.get_nest_props()
|
||||
if props.editing_nest and props.in_nest_mode:
|
||||
return tool.Ifc.get_entity(props.editing_nest)
|
||||
|
||||
@@ -419,7 +419,9 @@ class Root(bonsai.core.tool.Root):
|
||||
name = tool.Loader.get_name(element)
|
||||
if obj.name != name:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.is_renaming = True
|
||||
# If it's not an IFC object, then it doesn't have a name callback.
|
||||
# So, we need to protect it writing to IFC.
|
||||
props.is_renaming = bool(tool.Ifc.get_entity(obj))
|
||||
obj.name = name # The handler will trigger, and reset is_renaming to False
|
||||
|
||||
@classmethod
|
||||
@@ -428,7 +430,7 @@ class Root(bonsai.core.tool.Root):
|
||||
if material.name == name:
|
||||
return
|
||||
msprops = tool.Style.get_material_style_props(material)
|
||||
msprops.is_renaming = True
|
||||
msprops.is_renaming = bool(tool.Ifc.get_entity(material))
|
||||
material.name = name # The handler will trigger, and reset is_renaming to False.
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -25,7 +25,7 @@ import math
|
||||
import mathutils
|
||||
from mathutils import Matrix, Vector
|
||||
from lark import Lark, Transformer
|
||||
from typing import Union
|
||||
from typing import Union, Any
|
||||
|
||||
|
||||
class Snap(bonsai.core.tool.Snap):
|
||||
@@ -145,12 +145,12 @@ class Snap(bonsai.core.tool.Snap):
|
||||
|
||||
@classmethod
|
||||
def snap_on_axis(cls, intersection, tool_state):
|
||||
def create_axis_line_data(rot_mat, origin):
|
||||
def create_axis_line_data(rot_mat: Matrix, origin: Vector) -> tuple[Vector, Vector]:
|
||||
length = 1000
|
||||
direction = Vector((1, 0, 0))
|
||||
if tool_state.plane_method == "YZ" or (not tool_state.plane_method and tool_state.axis_method == "Z"):
|
||||
direction = Vector((0, 0, 1))
|
||||
rot_dir = rot_mat.inverted() @ direction
|
||||
rot_dir: Vector = rot_mat.inverted() @ direction
|
||||
start = origin + rot_dir * length
|
||||
end = origin - rot_dir * length
|
||||
|
||||
@@ -280,10 +280,16 @@ class Snap(bonsai.core.tool.Snap):
|
||||
return sorted_intersections
|
||||
|
||||
@classmethod
|
||||
def detect_snapping_points(cls, context: bpy.types.Context, event: bpy.types.Event, objs_2d_bbox, tool_state):
|
||||
def detect_snapping_points(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
event: bpy.types.Event,
|
||||
objs_2d_bbox: list[tuple[bpy.types.Object, list[float]]],
|
||||
tool_state: tool.Polyline.ToolState,
|
||||
) -> list[dict[str, Any]]:
|
||||
rv3d = context.region_data
|
||||
space = context.space_data
|
||||
detected_snaps = []
|
||||
detected_snaps: list[dict[str, Any]] = []
|
||||
|
||||
def select_plane_method():
|
||||
if not last_polyline_point:
|
||||
@@ -483,17 +489,17 @@ class Snap(bonsai.core.tool.Snap):
|
||||
|
||||
def sort_points_by_weighted_distance(snapping_points):
|
||||
for snap in snapping_points:
|
||||
weight_factor = 100 * cls.get_increment_snap_value(context)
|
||||
zoom_factor = bpy.context.region_data.view_distance
|
||||
if snap["type"] == "Vertex":
|
||||
snap["distance"] *= weight_factor / 12
|
||||
if snap["type"] == "Edge":
|
||||
snap["distance"] *= weight_factor
|
||||
snap["distance"] *= zoom_factor / 10
|
||||
if snap["type"] == "Edge Center":
|
||||
snap["distance"] *= weight_factor / 5
|
||||
snap["distance"] *= zoom_factor / 8
|
||||
if snap["type"] == "Edge Intersection":
|
||||
snap["distance"] *= weight_factor / 10
|
||||
snap["distance"] *= zoom_factor / 5
|
||||
if snap["type"] == "Edge":
|
||||
snap["distance"] *= zoom_factor
|
||||
if snap["type"] in ["Plane", "Axis", "Face"]:
|
||||
snap["distance"] = weight_factor / 50
|
||||
snap["distance"] *= zoom_factor
|
||||
return sorted(snapping_points, key=lambda x: x["distance"])
|
||||
|
||||
snaps_by_group = filter_snapping_points_by_group(detected_snaps)
|
||||
|
||||
@@ -172,3 +172,42 @@ class Structural(bonsai.core.tool.Structural):
|
||||
new = props.structural_analysis_models.add()
|
||||
new.ifc_definition_id = ifc_definition_id
|
||||
new.name = model["Name"] or "Unnamed"
|
||||
|
||||
@classmethod
|
||||
def get_vertex_representation(
|
||||
cls, product: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""
|
||||
:param product: IfcStructuralPointConnection
|
||||
:return: IfcTopologyRepresentation if it's valid.
|
||||
"""
|
||||
vertex_representation, undefined_representation = None, None
|
||||
# At least 1 representation is mandatory in IFC for IfcStructuralPointConnection.
|
||||
for rep in product.Representation.Representations:
|
||||
rep: ifcopenshell.entity_instance
|
||||
rep_type: str = rep.RepresentationType
|
||||
if rep_type == "Vertex":
|
||||
vertex_representation = rep
|
||||
break
|
||||
# It's possible to have 'Undefined' or some other non-predefined type.
|
||||
elif rep_type not in ("Edge", "Path", "Face", "Shell"):
|
||||
undefined_representation = rep
|
||||
|
||||
if not vertex_representation and not undefined_representation:
|
||||
return
|
||||
|
||||
# All other checks in this case are covered by IFC validation.
|
||||
if vertex_representation:
|
||||
items = vertex_representation.Items
|
||||
if len(items) != 1:
|
||||
return None
|
||||
return vertex_representation
|
||||
|
||||
if undefined_representation is None:
|
||||
return
|
||||
|
||||
items = undefined_representation.Items
|
||||
if len(items) != 1 or not all(item.is_a("IfcVertex") for item in items):
|
||||
return None
|
||||
|
||||
return undefined_representation
|
||||
|
||||
@@ -28,7 +28,7 @@ import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.helper
|
||||
from mathutils import Color
|
||||
from typing import Union, Any, Optional, Literal, TYPE_CHECKING
|
||||
from typing import Union, Any, Optional, Literal, TYPE_CHECKING, Sequence
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.style.prop import BIMStylesProperties, BIMStyleProperties
|
||||
@@ -291,7 +291,7 @@ class Style(bonsai.core.tool.Style):
|
||||
def get_surface_rendering_attributes(cls, obj: bpy.types.Material, verbose: bool = False) -> dict[str, Any]:
|
||||
report = (lambda *x: print(*x)) if verbose else (lambda *x: None)
|
||||
|
||||
def color_to_ifc_format(color):
|
||||
def color_to_ifc_format(color: Sequence[float]) -> dict[str, Any]:
|
||||
return {
|
||||
"Name": None,
|
||||
"Red": color[0],
|
||||
@@ -324,6 +324,7 @@ class Style(bonsai.core.tool.Style):
|
||||
report(f"{GREEN}Viewport color{R} saved as {GREEN}SurfaceColour{R}")
|
||||
|
||||
# TODO: make sure bsdf is connected to the output?
|
||||
assert obj.node_tree
|
||||
bsdfs = {n.type: n for n in obj.node_tree.nodes if n.outputs and n.outputs[0].is_linked}
|
||||
if "BSDF_PRINCIPLED" not in bsdfs:
|
||||
report(f"{GREEN}Viewport color alpha{R} saved as {GREEN}Transparency{R}")
|
||||
@@ -386,10 +387,10 @@ class Style(bonsai.core.tool.Style):
|
||||
report(f"BSDF {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}")
|
||||
diffuse_color = bsdf.inputs["Color"].default_value
|
||||
|
||||
elif "BSDF_DIFFUSE" in bsdfs:
|
||||
elif "BSDF_DIFFUSE" in bsdfs or "DIFFUSE_BSDF" in bsdfs:
|
||||
report(f"Because of {BLUE}BSDF_DIFFUSE{R} node reflectance method identified as {BLUE}MATT{R}")
|
||||
attributes["ReflectanceMethod"] = "MATT"
|
||||
bsdf = bsdfs["BSDF_DIFFUSE"]
|
||||
bsdf = bsdfs["BSDF_DIFFUSE"] if "BSDF_DIFFUSE" in bsdfs else bsdfs["DIFFUSE_BSDF"]
|
||||
|
||||
report(f"BSDF {GREEN}Roughness{R} saved as {GREEN}IfcSpecularRoughness{R}")
|
||||
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
|
||||
@@ -665,9 +666,12 @@ class Style(bonsai.core.tool.Style):
|
||||
@classmethod
|
||||
def rename_style(cls, style: ifcopenshell.entity_instance, name: str) -> None:
|
||||
"""Rename style and related blender material."""
|
||||
if style.Name == name:
|
||||
return
|
||||
style.Name = name
|
||||
blender_material = tool.Ifc.get_object(style)
|
||||
# Will implicitly update the style name using handler.
|
||||
blender_material.name = name
|
||||
assert isinstance(blender_material, bpy.types.Material)
|
||||
tool.Root.set_material_name(blender_material, name)
|
||||
|
||||
@classmethod
|
||||
def purge_unused_styles(cls) -> int:
|
||||
|
||||
@@ -30,6 +30,12 @@ pause
|
||||
echo Changing to the Git repository directory...
|
||||
cd %REPO_PATH%
|
||||
|
||||
:: Handle symlinks (they could be disabled by default on Windows).
|
||||
git config --local core.symlinks true
|
||||
:: Delete and checkout is the only way to ensure files are added as symlinks.
|
||||
del /Q src\bonsai\bonsai\bim\data\templates\projects\*.ifc
|
||||
git checkout -- src/bonsai/bonsai/bim/data/templates/projects/*.ifc
|
||||
|
||||
echo Copy over compiled IfcOpenShell files...
|
||||
copy "%PACKAGE_PATH%\ifcopenshell\*_wrapper*" "%CD%\src\ifcopenshell-python\ifcopenshell\"
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ PACKAGE_PATH="${BLENDER_PATH}/extensions/.local/lib/python3.11/site-packages"
|
||||
# BONSAI_PATH="${BLENDER_PATH}/extensions/user_default/bonsai"
|
||||
BONSAI_PATH="${BLENDER_PATH}/extensions/raw_githubusercontent_com/bonsai"
|
||||
|
||||
# Validate inputs.
|
||||
[ -z "$REPO_PATH" ] && echo "Error: REPO_PATH is not set." && exit 1
|
||||
[ -z "$BLENDER_PATH" ] && echo "Error: BLENDER_PATH is not set." && exit 1
|
||||
|
||||
# Changing to the Git repository directory
|
||||
cd "${REPO_PATH}"
|
||||
|
||||
|
||||
@@ -38,9 +38,10 @@ Scenario: Add aggregate
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I press "bim.add_aggregate"
|
||||
Then the object "IfcElementAssembly/Default_Name" exists
|
||||
@@ -51,9 +52,10 @@ Scenario: Add aggregate - add a nested aggregate
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I press "bim.add_aggregate"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
|
||||
@@ -38,14 +38,16 @@ Scenario: Copy attribute to selected
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcWall/Cube.001" is selected
|
||||
And I press "bim.enable_editing_attributes(obj='IfcWall/Cube.001')"
|
||||
|
||||
@@ -357,9 +357,10 @@ Scenario: Assign cost item quantity - count based
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
When I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
|
||||
Then nothing happens
|
||||
|
||||
@@ -372,9 +373,10 @@ Scenario: Assign cost item quantity - quantity based
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')"
|
||||
And I press "bim.perform_quantity_take_off"
|
||||
@@ -390,9 +392,10 @@ Scenario: Unassign cost item quantity - selection based
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
|
||||
When I press "bim.unassign_cost_item_quantity(cost_item={cost_item}, related_object=0)"
|
||||
@@ -407,9 +410,10 @@ Scenario: Unassign cost item quantity - explicit object
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
|
||||
And the variable "wall" is "{ifc}.by_type('IfcWall')[0].id()"
|
||||
@@ -425,9 +429,10 @@ Scenario: Select cost item products
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
|
||||
When I press "bim.select_cost_item_products(cost_item={cost_item})"
|
||||
@@ -442,9 +447,10 @@ Scenario: Select Cost Schedule Products
|
||||
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
|
||||
When I press "bim.select_cost_schedule_products(cost_schedule={cost_schedule})"
|
||||
|
||||
@@ -88,9 +88,10 @@ Scenario: Assign document
|
||||
And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
When I press "bim.assign_document(document={reference})"
|
||||
Then nothing happens
|
||||
|
||||
@@ -104,9 +105,10 @@ Scenario: Unassign document
|
||||
And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.assign_document(document={reference})"
|
||||
When I press "bim.unassign_document(document={reference})"
|
||||
Then nothing happens
|
||||
|
||||
@@ -5,9 +5,10 @@ Scenario: Duplicate drawing
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
@@ -22,9 +23,10 @@ Scenario: Create drawing
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
@@ -39,9 +41,10 @@ Scenario: Create drawing after deleting a duplicated object
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I duplicate the selected objects
|
||||
And I save sample test files
|
||||
And I look at the "Drawings" panel
|
||||
@@ -89,9 +92,10 @@ Scenario: Remove drawing
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
@@ -107,9 +111,10 @@ Scenario: Remove drawing - via object deletion
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_drawing"
|
||||
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
|
||||
And the collection "IfcAnnotation/PLAN_VIEW" exists
|
||||
@@ -121,9 +126,10 @@ Scenario: Remove drawing - deleting active drawing
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I save sample test files
|
||||
And I look at the "Drawings" panel
|
||||
And I click "IMPORT"
|
||||
|
||||
@@ -5,9 +5,10 @@ Scenario: Edit object placement
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I press "bim.edit_object_placement"
|
||||
Then nothing happens
|
||||
@@ -16,9 +17,10 @@ Scenario: Add representation
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
Then the object "IfcWall/Cube" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
When the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()"
|
||||
@@ -71,9 +73,10 @@ Scenario: Switch representation
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
|
||||
And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation})"
|
||||
Then nothing happens
|
||||
@@ -159,9 +162,10 @@ Scenario: Update representation - updating a tessellation
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.update_representation(obj='IfcWall/Cube')"
|
||||
Then the object "IfcWall/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
|
||||
@@ -169,9 +173,10 @@ Scenario: Update representation - updating a layered extrusion
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -196,9 +201,10 @@ Scenario: Update representation - updating a profiled extrusion
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -220,9 +226,10 @@ Scenario: Get representation IFC parameters
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.update_representation(ifc_representation_class='IfcExtrudedAreaSolid/IfcRectangleProfileDef')"
|
||||
When I press "bim.get_representation_ifc_parameters"
|
||||
Then nothing happens
|
||||
@@ -231,9 +238,10 @@ Scenario: Copy representation
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I press "bim.assign_class"
|
||||
@@ -253,9 +261,10 @@ Scenario: Override delete - with active IFC data
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I press "bim.override_object_delete"
|
||||
Then the object "IfcWall/Cube" does not exist
|
||||
@@ -278,9 +287,10 @@ Scenario: Override duplicate move - with active IFC data
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
|
||||
@@ -296,9 +306,10 @@ Scenario: Override duplicate move - with unlocked elements
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcBuildingStorey/My Storey" is selected
|
||||
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
|
||||
When I duplicate the selected objects
|
||||
@@ -309,9 +320,10 @@ Scenario: Override duplicate move - copying a coloured representation
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
|
||||
And I press "bim.enable_adding_presentation_style"
|
||||
@@ -348,9 +360,10 @@ Scenario: Override duplicate move - copying a layered extrusion
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -378,9 +391,10 @@ Scenario: Override duplicate move - copying a profiled extrusion
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -405,9 +419,10 @@ Scenario: Override duplicate move - copying an aggregate
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I press "bim.add_aggregate(aggregate_name='Assembly')"
|
||||
When the object "IfcWall/Cube" is selected
|
||||
@@ -488,9 +503,10 @@ Scenario: Override duplicate move linked - with active IFC data
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
When I press "object.duplicate_move_linked"
|
||||
@@ -517,9 +533,10 @@ Scenario: Override paste buffer - with active IFC data
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcBuildingStorey/My Storey" is selected
|
||||
When I press "view3d.copybuffer"
|
||||
|
||||
@@ -113,9 +113,10 @@ Scenario: Assign library reference
|
||||
And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I press "bim.assign_library_reference(reference={reference})"
|
||||
Then nothing happens
|
||||
@@ -129,9 +130,10 @@ Scenario: Unassign library reference
|
||||
And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.assign_library_reference(reference={reference})"
|
||||
When I press "bim.unassign_library_reference(reference={reference})"
|
||||
|
||||
@@ -45,9 +45,10 @@ Scenario: Assign material - single material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
|
||||
And I press "bim.assign_material"
|
||||
@@ -57,9 +58,10 @@ Scenario: Unassign material - single material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
|
||||
And I press "bim.assign_material"
|
||||
@@ -70,9 +72,10 @@ Scenario: Enable editing assigned material - single material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
|
||||
And I press "bim.assign_material"
|
||||
@@ -83,9 +86,10 @@ Scenario: Disable editing assigned material - single material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
|
||||
And I press "bim.assign_material"
|
||||
@@ -97,9 +101,10 @@ Scenario: Edit assigned material - single material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
|
||||
And I press "bim.assign_material"
|
||||
@@ -138,9 +143,10 @@ Scenario: Unassign material - removing inherited material
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
@@ -165,9 +171,10 @@ Scenario: Enable editing assigned material - material layer set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -183,9 +190,10 @@ Scenario: Disable editing assigned material - material layer set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -202,9 +210,10 @@ Scenario: Edit assigned material - material layer set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -248,9 +257,10 @@ Scenario: Enable editing assigned material - material profile set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -266,9 +276,10 @@ Scenario: Disable editing assigned material - material profile set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -285,9 +296,10 @@ Scenario: Edit assigned material - material profile set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -305,9 +317,10 @@ Scenario: Assign material - material constituent set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
|
||||
And I press "bim.assign_material"
|
||||
@@ -317,9 +330,10 @@ Scenario: Unassign material - material constituent set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
|
||||
And I press "bim.assign_material"
|
||||
@@ -330,9 +344,10 @@ Scenario: Enable editing assigned material - material constituent set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
|
||||
And I press "bim.assign_material"
|
||||
@@ -343,9 +358,10 @@ Scenario: Disable editing assigned material - material constituent set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
|
||||
And I press "bim.assign_material"
|
||||
@@ -357,9 +373,10 @@ Scenario: Edit assigned material - material constituent set
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I press "bim.add_material()"
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
|
||||
And I press "bim.assign_material"
|
||||
@@ -409,9 +426,10 @@ Scenario: Add material set layer
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
@@ -429,9 +447,10 @@ Scenario: Remove material set layer
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
|
||||
And I add an empty
|
||||
And the object "Empty" is selected
|
||||
|
||||
@@ -21,23 +21,28 @@ Scenario: Resize to storey
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
|
||||
And I press "bim.set_default_container(container={storey})"
|
||||
And I press "bim.assign_container(container={storey})"
|
||||
When I press "bim.resize_to_storey(total_storeys=1)"
|
||||
And I look at the "Spatial Decomposition" panel
|
||||
And I select the "My Storey" item in the "BIM_UL_containers_manager" list
|
||||
And I click "Set Default"
|
||||
# Assign Container.
|
||||
And I click "FOLDER_REDIRECT"
|
||||
And I look at the "Miscellaneous" panel
|
||||
When I click "Resize To Storey"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Split along edge - boolean mode
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add a plane of size "4" at "0,0,0"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "Plane" is selected
|
||||
@@ -50,9 +55,10 @@ Scenario: Split along edge - bisect mode
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add a plane of size "4" at "0,0,0"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "Plane" is selected
|
||||
|
||||
@@ -284,9 +284,10 @@ Scenario: Assign actor
|
||||
And I press "bim.add_actor"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When I press "bim.assign_actor(actor={actor})"
|
||||
@@ -298,9 +299,10 @@ Scenario: Unassign actor
|
||||
And I press "bim.add_actor"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.assign_actor(actor={actor})"
|
||||
|
||||
@@ -901,9 +901,10 @@ Scenario: Export IFC - with changed object scale ignored
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
When the object "IfcWall/Cube" is scaled to "2"
|
||||
And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
|
||||
|
||||
@@ -5,9 +5,10 @@ Scenario: Add pset - object
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
When I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
|
||||
@@ -17,15 +18,17 @@ Scenario: Add pset - multiple objects
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcWall/Cube.001" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
@@ -36,9 +39,10 @@ Scenario: Enable pset editing - object
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
|
||||
@@ -128,9 +132,10 @@ Scenario: Disable pset editing - object
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
|
||||
@@ -202,9 +207,10 @@ Scenario: Edit pset - object
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
|
||||
@@ -215,9 +221,10 @@ Scenario: Edit qto - object
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I set "active_object.PsetProperties.qto_name" to "Qto_WallBaseQuantities"
|
||||
When I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')"
|
||||
@@ -290,14 +297,16 @@ Scenario: Copy property to selected - copy property
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcWall/Cube.001" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
@@ -310,9 +319,10 @@ Scenario: Remove pset - object
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
|
||||
@@ -326,15 +336,17 @@ Scenario: Remove pset - multiple objects
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I look at the "Class" panel
|
||||
And I set the "Products" property to "IfcElement"
|
||||
And I set the "Class" property to "IfcWall"
|
||||
And I click "Assign IFC Class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And additionally the object "IfcWall/Cube.001" is selected
|
||||
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user