mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-30 00:23:14 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efede9eb03 | |||
| ac315e8a55 | |||
| b7c18221f4 | |||
| 7a49f1611e | |||
| 5c0a7af830 | |||
| a968b61d02 | |||
| 0ac9d739f8 | |||
| 74c6480808 | |||
| 0601802005 | |||
| 9784fb1c49 | |||
| 039dcbaefa | |||
| 1cd17fd5f8 | |||
| 2f0f9fe08b | |||
| 50ed75fcc2 | |||
| 9eaf41c9f0 | |||
| 581a674654 | |||
| e29f686d3f | |||
| 8cf7057abc | |||
| 316b9554d4 | |||
| 95fa6f39c4 | |||
| 65ffbe081b | |||
| 68f175fc71 | |||
| a428169a13 | |||
| 7db0196791 | |||
| 4f4c557f61 | |||
| aafb6fc426 | |||
| a6e953a550 |
+6
-1
@@ -330,7 +330,12 @@ endif
|
||||
# Use '-n' as on macos x86-64 doesn't have a binary wheel and it autoincludes 'cp311'.
|
||||
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
|
||||
whl_name=$$(echo $$prev_whl_name | sed "s/-cp39-/-cp$(PYNUMBER)-/"); \
|
||||
mv -n "$$prev_whl_name" "$$whl_name";
|
||||
echo "$$prev_whl_name $$whl_name"; \
|
||||
if [ "$$(realpath $$prev_whl_name)" = "$$(realpath $$whl_name)" ]; then \
|
||||
echo "Source and destination are the same file. Skipping move."; \
|
||||
else \
|
||||
mv -n "$$prev_whl_name" "$$whl_name"; \
|
||||
fi
|
||||
|
||||
ifneq ($(PLATFORM), linux)
|
||||
# Safeguard: in case one of `pip download` will break,
|
||||
|
||||
Binary file not shown.
@@ -261,8 +261,6 @@ class IfcImporter:
|
||||
self.profile_code("Setup arrays")
|
||||
tool.Project.load_linked_models_from_ifc()
|
||||
self.profile_code("Load linked models")
|
||||
self.lock_scales()
|
||||
self.profile_code("Lock objects scales")
|
||||
self.add_project_to_scene()
|
||||
self.profile_code("Add project to scene")
|
||||
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
||||
@@ -1129,20 +1127,6 @@ class IfcImporter:
|
||||
tool.Aggregate.constrain_all_parts_to_aggregate(relating_obj)
|
||||
bpy.context.scene.BIMAggregateProperties.aggregate_decorator = True
|
||||
|
||||
def lock_scales(self) -> None:
|
||||
elements = set(self.file.by_type("IfcProduct"))
|
||||
while elements:
|
||||
element = elements.pop()
|
||||
if not getattr(element, "HasOpenings", False):
|
||||
continue
|
||||
voided_elements = tool.Aggregate.get_parts_recursively(element)
|
||||
voided_elements.add(element)
|
||||
elements.difference_update(voided_elements)
|
||||
for element in voided_elements:
|
||||
if not (obj := tool.Ifc.get_object(element)):
|
||||
continue
|
||||
tool.Geometry.lock_scale(obj)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
def __init__(self):
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
classes = (
|
||||
operator.AddCurvelikeItem,
|
||||
@@ -89,7 +90,20 @@ classes = (
|
||||
addon_keymaps = []
|
||||
|
||||
|
||||
@persistent
|
||||
def block_scale(scene):
|
||||
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
|
||||
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.app.handlers.depsgraph_update_pre.append(block_scale)
|
||||
|
||||
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
|
||||
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
||||
@@ -158,6 +172,8 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.app.handlers.depsgraph_update_pre.remove(block_scale)
|
||||
|
||||
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
|
||||
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
|
||||
|
||||
@@ -57,20 +57,23 @@ class AuthoringData:
|
||||
cls.data["ifc_element_type"] = cls.ifc_element_type
|
||||
cls.data["ifc_classes"] = cls.ifc_classes()
|
||||
cls.data["ifc_class_current"] = cls.ifc_class_current()
|
||||
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
|
||||
cls.data["relating_type_id_current"] = cls.relating_type_id_current() # only after .ifc_classes()
|
||||
cls.data["relating_type_name"] = cls.relating_type_name() # only after .relating_type_id()
|
||||
cls.data["relating_type_description"] = cls.relating_type_description() # only after .relating_type_id()
|
||||
cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id()
|
||||
|
||||
# Only after .ifc_classes()
|
||||
# Make sure .ifc_classes() was run before next lines
|
||||
cls.data["type_elements"] = cls.type_elements()
|
||||
cls.data["type_elements_filtered"] = cls.type_elements_filtered()
|
||||
cls.data["relating_type_id"] = cls.relating_type_id()
|
||||
# Make sure .relating_type_id() was run before next lines
|
||||
cls.data["relating_type_id_current"] = cls.relating_type_id_current()
|
||||
cls.data["relating_type_name"] = cls.relating_type_name()
|
||||
cls.data["relating_type_description"] = cls.relating_type_description()
|
||||
cls.data["predefined_type"] = cls.predefined_type()
|
||||
# Make sure .type_elements_filtered() was run before next lines
|
||||
cls.data["total_types"] = cls.total_types()
|
||||
cls.data["total_pages"] = cls.total_pages()
|
||||
cls.data["total_pages"] = cls.total_pages() # Only after .total_types()
|
||||
cls.data["next_page"] = cls.next_page()
|
||||
cls.data["prev_page"] = cls.prev_page()
|
||||
cls.data["paginated_relating_types"] = cls.paginated_relating_types()
|
||||
|
||||
cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id()
|
||||
cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id_current()
|
||||
cls.data["is_voidable_element"] = cls.is_voidable_element()
|
||||
cls.data["has_visible_openings"] = cls.has_visible_openings()
|
||||
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
|
||||
@@ -104,23 +107,15 @@ class AuthoringData:
|
||||
|
||||
@classmethod
|
||||
def type_thumbnail(cls):
|
||||
if not cls.data["relating_type_id"]:
|
||||
return 0
|
||||
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
|
||||
if relating_type_id is None:
|
||||
return 0
|
||||
element = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
return cls.type_thumbnails.get(element.id(), None) or 0
|
||||
return cls.type_thumbnails.get(int(cls.data["relating_type_id_current"] or 0), 0)
|
||||
|
||||
@classmethod
|
||||
def total_types(cls):
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
return len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0
|
||||
return len(cls.data["type_elements_filtered"])
|
||||
|
||||
@classmethod
|
||||
def total_pages(cls):
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
total_types = len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0
|
||||
total_types = cls.data["total_types"]
|
||||
return math.ceil(total_types / cls.types_per_page)
|
||||
|
||||
@classmethod
|
||||
@@ -134,12 +129,35 @@ class AuthoringData:
|
||||
return cls.props.type_page - 1
|
||||
|
||||
@classmethod
|
||||
def paginated_relating_types(cls):
|
||||
def type_elements(cls):
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
if not ifc_class:
|
||||
return []
|
||||
elements = list(tool.Ifc.get().by_type(ifc_class))
|
||||
return natsorted(elements, key=lambda s: (s.Name or "Unnamed").lower())
|
||||
|
||||
@classmethod
|
||||
def type_elements_filtered(cls):
|
||||
search_query = cls.props.search_name.lower()
|
||||
|
||||
def filter_element(element):
|
||||
if search_query in (element.Name or "Unnamed").lower():
|
||||
return True
|
||||
if search_query in (element.Description or "").lower():
|
||||
return True
|
||||
if search_query in (ifcopenshell.util.element.get_predefined_type(element) or "").lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
elements = cls.data["type_elements"]
|
||||
if cls.props.search_name:
|
||||
return [e for e in elements if filter_element(e)]
|
||||
return elements
|
||||
|
||||
@classmethod
|
||||
def paginated_relating_types(cls):
|
||||
results = []
|
||||
elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda e: e.Name or "Unnamed")
|
||||
elements = cls.data["type_elements_filtered"]
|
||||
elements = elements[(cls.props.type_page - 1) * cls.types_per_page : cls.props.type_page * cls.types_per_page]
|
||||
for element in elements:
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
@@ -266,13 +284,8 @@ class AuthoringData:
|
||||
|
||||
@classmethod
|
||||
def relating_type_id(cls):
|
||||
results = []
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
if ifc_class:
|
||||
elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: (s.Name or "Unnamed").lower())
|
||||
results.extend(elements)
|
||||
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results]
|
||||
return []
|
||||
elements = cls.data["type_elements"]
|
||||
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in elements]
|
||||
|
||||
@classmethod
|
||||
def relating_type_id_current(cls):
|
||||
|
||||
@@ -491,7 +491,6 @@ class SetActiveType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
props.relating_type_id = str(self.relating_type)
|
||||
context.window.screen = context.window.screen # Closes the type manager popup
|
||||
|
||||
|
||||
class AlignProduct(bpy.types.Operator):
|
||||
@@ -566,10 +565,11 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
processing = set()
|
||||
# Only process at most one paginated class at a time.
|
||||
# Large projects have hundreds of types which can lead to unnecessary lag.
|
||||
queue = sorted(tool.Ifc.get().by_type(self.ifc_class), key=lambda e: e.Name or "Unnamed")
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
queue = AuthoringData.data["type_elements_filtered"]
|
||||
if self.limit:
|
||||
queue = queue[self.offset : self.offset + self.limit]
|
||||
else:
|
||||
@@ -578,6 +578,12 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
|
||||
offset = 0
|
||||
queue = queue[offset : offset + 9]
|
||||
|
||||
# The active type may be in another page than the active one :
|
||||
if relating_type_id_current := AuthoringData.data["relating_type_id_current"]:
|
||||
active_element = tool.Ifc.get_entity_by_id(int(relating_type_id_current))
|
||||
if active_element and active_element not in queue:
|
||||
queue.append(active_element)
|
||||
|
||||
while queue:
|
||||
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
|
||||
element = queue.pop()
|
||||
|
||||
@@ -43,6 +43,9 @@ def get_boundary_class(self, context):
|
||||
def get_relating_type_id(self, context):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
else:
|
||||
AuthoringData.data["type_elements"] = AuthoringData.type_elements()
|
||||
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
|
||||
return AuthoringData.data["relating_type_id"]
|
||||
|
||||
|
||||
@@ -65,10 +68,14 @@ def update_relating_type_id(self, context):
|
||||
AuthoringData.data["relating_type_name"] = AuthoringData.relating_type_name()
|
||||
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
|
||||
AuthoringData.data["predefined_type"] = AuthoringData.predefined_type()
|
||||
self.type_page = [e[0] for e in AuthoringData.data["relating_type_id"]].index(self.relating_type_id) // 9 + 1
|
||||
|
||||
|
||||
def update_type_page(self, context):
|
||||
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class, offset=9 * (self.type_page - 1), limit=9)
|
||||
self["type_page"] = min(self["type_page"], AuthoringData.data["total_pages"])
|
||||
self["type_page"] = max(self["type_page"], 1)
|
||||
|
||||
|
||||
def update_relating_array_from_object(self, context):
|
||||
@@ -97,6 +104,14 @@ def update_slab_direction_decorator(self, context):
|
||||
SlabDirectionDecorator.uninstall()
|
||||
|
||||
|
||||
def update_search_name(self, context):
|
||||
AuthoringData.load()
|
||||
# Total number of pages may decrease when using the search bar :
|
||||
if self.type_page > AuthoringData.data["total_pages"]:
|
||||
self.type_page = max(1, AuthoringData.data["total_pages"])
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
|
||||
|
||||
|
||||
def update_x_angle(self, context):
|
||||
angle_deg = math.degrees(self.x_angle)
|
||||
if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5):
|
||||
@@ -108,6 +123,13 @@ class BIMModelProperties(PropertyGroup):
|
||||
relating_type_id: bpy.props.EnumProperty(
|
||||
items=get_relating_type_id, name="Relating Type", update=update_relating_type_id
|
||||
)
|
||||
search_name: bpy.props.StringProperty(
|
||||
name="Search Name",
|
||||
default="",
|
||||
description="Use this property to filter the list of available types",
|
||||
update=update_search_name,
|
||||
options={"SKIP_SAVE", "TEXTEDIT_UPDATE"},
|
||||
)
|
||||
menu_relating_type_id: bpy.props.IntProperty()
|
||||
icon_id: bpy.props.IntProperty()
|
||||
updating: bpy.props.BoolProperty(default=False)
|
||||
@@ -167,10 +189,10 @@ class BIMModelProperties(PropertyGroup):
|
||||
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
|
||||
# Used for plan calculation points such as in room generation
|
||||
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
|
||||
x_angle: bpy.props.FloatProperty(
|
||||
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
|
||||
)
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
|
||||
direction_sense: bpy.props.EnumProperty(
|
||||
@@ -228,6 +250,24 @@ class BIMArrayProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
|
||||
def update_total_length_target(self, context):
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
|
||||
|
||||
def update_tread_run(self, context):
|
||||
if self.total_length_lock:
|
||||
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
|
||||
|
||||
def update_number_of_treads(self, context):
|
||||
if self.total_length_lock:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
def validate_nosing_value(self, context):
|
||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||
@@ -243,9 +283,26 @@ class BIMStairProperties(PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01, subtype="DISTANCE")
|
||||
number_of_treads: bpy.props.IntProperty(name="Number of Treads", default=6, soft_min=1)
|
||||
number_of_treads: bpy.props.IntProperty(
|
||||
name="Number of Treads", default=6, soft_min=1, update=update_number_of_treads
|
||||
)
|
||||
total_length_target: bpy.props.FloatProperty(
|
||||
name="Total Length Target",
|
||||
default=3.0,
|
||||
soft_min=0.01,
|
||||
subtype="DISTANCE",
|
||||
update=update_total_length_target,
|
||||
description="Total Length Target, might not be exactly respected depending on the parameters",
|
||||
)
|
||||
total_length_lock: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Lock Total Length",
|
||||
description="Lock Total Length when changing number of treads or tread run",
|
||||
)
|
||||
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01, subtype="DISTANCE")
|
||||
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE")
|
||||
tread_run: bpy.props.FloatProperty(
|
||||
name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE", update=update_tread_run
|
||||
)
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
|
||||
@@ -84,10 +84,9 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
bl_idname = "bim.launch_type_manager"
|
||||
bl_label = "Launch Type Manager"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Display all available Construction Types to add new instances"
|
||||
bl_description = "Browse, Edit and Manage Types"
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.bim.hotkey("INVOKE_DEFAULT", hotkey="S_A")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -101,40 +100,42 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
# will be None if project has no types
|
||||
if ifc_class is not None:
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
|
||||
return context.window_manager.invoke_props_dialog(
|
||||
self, width=550, title="Type Manager", confirm_text="Add Type"
|
||||
)
|
||||
return context.window_manager.invoke_props_dialog(self, width=550, title="Type Manager", confirm_text="Close")
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
row = self.layout.row(align=True)
|
||||
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}"
|
||||
if AuthoringData.data["total_types"] > 1:
|
||||
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}s"
|
||||
else:
|
||||
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}"
|
||||
text += "s"
|
||||
row.label(text=text, icon="FILE_VOLUME")
|
||||
# prop_with_search(row, props, "ifc_class", text="", should_click_ok_to_validate=True)
|
||||
row.menu("BIM_MT_type_manager_menu", text="", icon="PREFERENCES")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.launch_add_element", text=f"Create New {AuthoringData.data['ifc_element_type']}", icon="ADD")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
# row.prop(props, "relating_type_id", text="")
|
||||
row.prop(props, "search_name", icon="FILTER", text="")
|
||||
|
||||
columns = self.layout.column_flow(columns=3)
|
||||
row = columns.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
### In case you want something here in the future
|
||||
if AuthoringData.data["total_pages"] > 0:
|
||||
row = columns.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row2 = row.row(align=True)
|
||||
row2.label(text="Page")
|
||||
row2.prop(props, "type_page", text="", emboss=False)
|
||||
row2.label(text=f"/{AuthoringData.data['total_pages']} ")
|
||||
|
||||
###
|
||||
prev_page_op = row.row(align=True)
|
||||
op = prev_page_op.operator("bim.change_type_page", icon="TRIA_LEFT", text="")
|
||||
op.page = AuthoringData.data["prev_page"] or 0
|
||||
prev_page_op.enabled = op.page > 0
|
||||
|
||||
row = columns.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
if AuthoringData.data["total_pages"] > 1:
|
||||
row.label(text=f"Page {props.type_page}/{AuthoringData.data['total_pages']} ")
|
||||
if AuthoringData.data["prev_page"]:
|
||||
op = row.operator("bim.change_type_page", icon="TRIA_LEFT", text="")
|
||||
op.page = AuthoringData.data["prev_page"]
|
||||
if AuthoringData.data["next_page"]:
|
||||
op = row.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
|
||||
op.page = AuthoringData.data["next_page"]
|
||||
next_page_op = row.row(align=True)
|
||||
op = next_page_op.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
|
||||
op.page = AuthoringData.data["next_page"] or 0
|
||||
next_page_op.enabled = op.page > 0
|
||||
|
||||
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
|
||||
|
||||
@@ -142,11 +143,11 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
outer_col = flow.column()
|
||||
box = outer_col.box()
|
||||
|
||||
row = box.row(align=True)
|
||||
row = box.row()
|
||||
op = row.operator("bim.set_active_type", text=relating_type["name"], icon="BLANK1", emboss=False)
|
||||
op.relating_type = relating_type["id"]
|
||||
|
||||
op = row.operator("bim.launch_type_menu", icon="DOWNARROW_HLT", text="", emboss=False)
|
||||
op = row.operator("bim.launch_type_menu", icon="PREFERENCES", text="", emboss=True)
|
||||
op.relating_type_id = relating_type["id"]
|
||||
|
||||
row = box.row()
|
||||
@@ -163,7 +164,15 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
is_current_relating_type = str(relating_type["id"]) == str(
|
||||
AuthoringData.data["relating_type_id_current"]
|
||||
)
|
||||
if is_current_relating_type:
|
||||
active_row = row2.row()
|
||||
active_row.alignment = "CENTER"
|
||||
active_row.label(text="Active", icon="CHECKMARK")
|
||||
else:
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
else:
|
||||
row = box.row()
|
||||
op = box.operator("bim.load_type_thumbnails", text="", icon="FILE_REFRESH", emboss=False)
|
||||
@@ -302,6 +311,15 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
else:
|
||||
self.layout.prop(props, prop_name)
|
||||
if prop_name == "height": # Weak but we just want to insert this inside props drawing
|
||||
row_length = self.layout.row(align=True)
|
||||
row_length.prop(props, "total_length_target")
|
||||
row_length.prop(
|
||||
props,
|
||||
"total_length_lock",
|
||||
text="",
|
||||
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
|
||||
)
|
||||
regenerate_stair_mesh(obj)
|
||||
else:
|
||||
calculated_params = StairData.data["calculated_params"]
|
||||
|
||||
@@ -436,8 +436,9 @@ class CreateObjectUI:
|
||||
row = cls.layout.row(align=True)
|
||||
if AuthoringData.data["relating_type_id"]:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id)
|
||||
op.hotkey = "S_A"
|
||||
if context.space_data.type == "VIEW_3D": # Wall polyline tool works only in 3D Space
|
||||
op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id)
|
||||
op.hotkey = "S_A"
|
||||
else:
|
||||
row.label(text="No Construction Type", icon="FILE_3D")
|
||||
|
||||
@@ -500,62 +501,58 @@ class CreateObjectUI:
|
||||
row = cls.layout.row(align=True)
|
||||
if not AuthoringData.data["ifc_element_type"]:
|
||||
prop_with_search(row, cls.props, "ifc_class", text="Type Class" if ui_context != "TOOL_HEADER" else "")
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
ifc_class = AuthoringData.data["ifc_class_current"]
|
||||
if ifc_class:
|
||||
box = cls.layout.box()
|
||||
row = box.row(align=True)
|
||||
if AuthoringData.data["type_thumbnail"] and ui_context == "TOOL_HEADER":
|
||||
row.template_icon(icon_value=AuthoringData.data["type_thumbnail"])
|
||||
row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False)
|
||||
else:
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
icon="BLANK1",
|
||||
text=AuthoringData.data["relating_type_name"],
|
||||
emboss=False,
|
||||
)
|
||||
if not AuthoringData.data["ifc_classes"]:
|
||||
return
|
||||
if not (ifc_class := AuthoringData.data["ifc_class_current"]):
|
||||
return
|
||||
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
icon=tool.Blender.TYPE_MANAGER_ICON,
|
||||
text="",
|
||||
emboss=False,
|
||||
)
|
||||
box = cls.layout.box()
|
||||
|
||||
if ui_context != "TOOL_HEADER":
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["relating_type_description"],
|
||||
emboss=False,
|
||||
)
|
||||
row = box.row(align=True)
|
||||
thumbnail: int = AuthoringData.data["type_thumbnail"]
|
||||
row.template_icon(icon_value=thumbnail)
|
||||
row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False)
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
icon=tool.Blender.TYPE_MANAGER_ICON,
|
||||
text="",
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
if AuthoringData.data["type_thumbnail"]:
|
||||
row1 = box.row()
|
||||
row1.ui_units_y = 0.01
|
||||
row1.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=4)
|
||||
row2 = box.column(align=True)
|
||||
row2.ui_units_y = 4
|
||||
for _ in range(4):
|
||||
row2.operator("bim.launch_type_manager", text="", emboss=False)
|
||||
else:
|
||||
op = box.operator(
|
||||
"bim.load_type_thumbnails",
|
||||
text="",
|
||||
icon="FILE_REFRESH",
|
||||
emboss=False,
|
||||
)
|
||||
op.ifc_class = ifc_class
|
||||
if ui_context == "TOOL_HEADER":
|
||||
return
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["relating_type_description"],
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["predefined_type"],
|
||||
emboss=False,
|
||||
)
|
||||
if thumbnail != 0:
|
||||
row1 = box.row()
|
||||
row1.ui_units_y = 0.01
|
||||
row1.template_icon(icon_value=thumbnail, scale=4)
|
||||
row2 = box.column(align=True)
|
||||
row2.ui_units_y = 4
|
||||
for _ in range(4):
|
||||
row2.operator("bim.launch_type_manager", text="", emboss=False)
|
||||
else:
|
||||
op = box.operator(
|
||||
"bim.load_type_thumbnails",
|
||||
text="",
|
||||
icon="FILE_REFRESH",
|
||||
emboss=False,
|
||||
)
|
||||
op.ifc_class = ifc_class
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["predefined_type"],
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
|
||||
class EditObjectUI:
|
||||
@@ -963,16 +960,18 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item")
|
||||
else:
|
||||
# Slab from walls
|
||||
walls = False
|
||||
# Make sure only Ifc Walls are selected
|
||||
for obj in bpy.context.selected_objects:
|
||||
walls = tool.Ifc.get_entity(obj).is_a("IfcWall")
|
||||
if (
|
||||
walls
|
||||
and relating_type_id
|
||||
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
|
||||
):
|
||||
bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcWall"):
|
||||
break
|
||||
else:
|
||||
if (
|
||||
relating_type_id
|
||||
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
|
||||
):
|
||||
bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
# Walls from slab
|
||||
slab = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if (
|
||||
|
||||
@@ -546,6 +546,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif representation_template == "ROOF":
|
||||
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||
bpy.ops.bim.add_roof()
|
||||
tool.Blender.set_active_object(obj)
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMRootProperties
|
||||
|
||||
@@ -43,6 +43,7 @@ class Collector(bonsai.core.tool.Collector):
|
||||
|
||||
# Note that tool.Geometry.is_locked is only checked within the if
|
||||
# statements for efficiency as it is a slow check.
|
||||
tool.Geometry.lock_scale(obj)
|
||||
|
||||
if element.is_a("IfcGridAxis"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
|
||||
@@ -132,7 +132,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
obj.lock_rotation = (True, True, True)
|
||||
obj.lock_rotation_w = True
|
||||
obj.lock_rotations_4d = True
|
||||
obj.lock_scale = (True, True, True)
|
||||
|
||||
@classmethod
|
||||
def unlock_object(cls, obj: bpy.types.Object) -> None:
|
||||
@@ -140,7 +139,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
obj.lock_rotation = (False, False, False)
|
||||
obj.lock_rotation_w = False
|
||||
obj.lock_rotations_4d = False
|
||||
obj.lock_scale = (False, False, False)
|
||||
|
||||
@classmethod
|
||||
def lock_scale(cls, obj: bpy.types.Object) -> None:
|
||||
|
||||
@@ -49,6 +49,7 @@ __version__ = version = "0.0.0"
|
||||
|
||||
|
||||
ParserPreset = Literal["basic", "cobie24", "cobie24legacy"]
|
||||
GetElementDataCallBack = Callable[[ifcopenshell.file, ifcopenshell.entity_instance], dict[str, Any]]
|
||||
_parser_presets_configs = {}
|
||||
|
||||
|
||||
@@ -69,9 +70,7 @@ def get_presets_configs() -> dict[ParserPreset, dict[str, Any]]:
|
||||
class Parser:
|
||||
config: dict[str, Any]
|
||||
categories: defaultdict[str, dict[str, Any]]
|
||||
get_custom_element_data: dict[
|
||||
str, Union[Callable[[ifcopenshell.file, ifcopenshell.entity_instance], dict[str, Any]], dict[str, Any]]
|
||||
]
|
||||
get_custom_element_data: dict[str, Union[GetElementDataCallBack, dict[str, Any]]]
|
||||
duplicate_keys: list[tuple[dict[str, Any], dict[str, Any]]]
|
||||
|
||||
def __init__(self, preset: Union[str, ParserPreset, dict[str, Any]] = "basic"):
|
||||
@@ -93,6 +92,7 @@ class Parser:
|
||||
def parse(self, ifc_file: ifcopenshell.file, name=None):
|
||||
for category_name, category_config in self.config["categories"].items():
|
||||
for element in category_config["get_category_elements"](ifc_file):
|
||||
get_element_data: Union[GetElementDataCallBack, dict[str, Any]]
|
||||
get_element_data = category_config["get_element_data"]
|
||||
|
||||
if isinstance(get_element_data, dict):
|
||||
@@ -271,19 +271,23 @@ class Writer:
|
||||
def write_xlsx(self, output: str) -> None:
|
||||
workbook = Workbook()
|
||||
|
||||
cell_formats = {}
|
||||
cell_formats: dict[str, PatternFill] = {}
|
||||
for key, value in self.config.get("colours", {}).items():
|
||||
fill = PatternFill(start_color=value, end_color=value, fill_type="solid")
|
||||
cell_formats[key] = fill
|
||||
|
||||
for category, data in self.categories.items():
|
||||
colours = self.config.get("categories", {}).get(category, {}).get("colours", [])
|
||||
category_data = self.config.get("categories", {}).get(category, {})
|
||||
colours = category_data.get("colours", ())
|
||||
|
||||
if category in workbook.sheetnames:
|
||||
worksheet = workbook[category]
|
||||
else:
|
||||
worksheet = workbook.create_sheet(category)
|
||||
|
||||
if category_colour := category_data.get("colour", None):
|
||||
worksheet.sheet_properties.tabColor = cell_formats[category_colour].start_color.rgb
|
||||
|
||||
r = 1 # Openpyxl uses 1-based indexing
|
||||
c = 1
|
||||
for header in data["headers"]:
|
||||
|
||||
+119
-7
@@ -17,13 +17,16 @@
|
||||
# along with IfcFM. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.classification
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.fm
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.system
|
||||
from typing import Any, Union, Optional
|
||||
from ifcopenshell.util.shape_builder import np_matrix_to_euler
|
||||
from typing import Any, Union, Optional, Generator
|
||||
|
||||
|
||||
# The original BIMServer plugin has a function called ifcToCOBie:
|
||||
@@ -163,7 +166,7 @@ def get_documents(
|
||||
|
||||
|
||||
def get_attributes(ifc_file: ifcopenshell.file) -> list[dict[str, Any]]:
|
||||
results = []
|
||||
results: list[dict[str, Any]] = []
|
||||
history = get_history(ifc_file)
|
||||
created_by = get_email_from_history(history) if history else None
|
||||
created_on = ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None
|
||||
@@ -616,7 +619,6 @@ def get_component_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity
|
||||
|
||||
def get_system_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
||||
system, component = element
|
||||
category = get_category(system)
|
||||
component_name = val(component.Name)
|
||||
return {
|
||||
"Name": val(system.Name),
|
||||
@@ -851,12 +853,81 @@ def get_document_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_
|
||||
}
|
||||
|
||||
|
||||
def get_attribute_data(
|
||||
ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance
|
||||
) -> ifcopenshell.entity_instance:
|
||||
def pass_category_elements_as_data(ifc_file: ifcopenshell.file, element: dict[str, Any]) -> dict[str, Any]:
|
||||
return element
|
||||
|
||||
|
||||
def get_coordinate_elements(ifc_file: ifcopenshell.file) -> list[dict[str, Any]]:
|
||||
elements = get_floors(ifc_file) + get_spaces(ifc_file)
|
||||
results: list[dict[str, Any]] = []
|
||||
for element in elements:
|
||||
for data in get_coordinate_data_(element):
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
def get_coordinate_data_(element: ifcopenshell.entity_instance) -> Generator[dict[str, Any], None, None]:
|
||||
M_TRANSLATION = (slice(0, 3), 3)
|
||||
element_name = val(element.Name)
|
||||
element_class = element.is_a()
|
||||
|
||||
base_data = {
|
||||
"CreatedBy": get_created_by(element),
|
||||
"CreatedOn": get_created_on(element),
|
||||
"RowName": element_name,
|
||||
"ExtSystem": "IfcFm",
|
||||
"ExtObject": element_class,
|
||||
"ExtIdentifier": element.GlobalId,
|
||||
}
|
||||
|
||||
if element_class == "IfcBuildingStorey":
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
categories = ("points",)
|
||||
rotation = np_matrix_to_euler(matrix)
|
||||
translation = matrix[M_TRANSLATION]
|
||||
# Rotation axes as explained in https://www.nibs.org/nbims/v3/cobie.
|
||||
points_data = {
|
||||
"Name": element_name,
|
||||
"Category": "point",
|
||||
"SheetName": "Floor",
|
||||
"CoordinateXAxis": translation[0],
|
||||
"CoordinateYAxis": translation[1],
|
||||
"CoordinateZAxis": translation[2],
|
||||
"ClockwiseRotation": rotation[2] * 180,
|
||||
"ElevationalRotation": rotation[0] * 180,
|
||||
"YawRotation": rotation[1] * 180,
|
||||
}
|
||||
yield base_data | points_data
|
||||
return
|
||||
|
||||
# IfcSpaces.
|
||||
if element.Representation is None:
|
||||
return
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape: ifcopenshell.geom.ShapeElementType
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry)
|
||||
categories = ("box-lowerleft", "box-upperright")
|
||||
bbox = ifcopenshell.util.shape.get_bbox(verts)
|
||||
base_data = base_data | {
|
||||
"Category": "point",
|
||||
"SheetName": "Space",
|
||||
# Spaces don't need rotation as box points are already in world space.
|
||||
"ClockwiseRotation": None,
|
||||
"ElevationalRotation": None,
|
||||
"YawRotation": None,
|
||||
}
|
||||
for category, point in zip(categories, bbox):
|
||||
box_point_data = {
|
||||
"Name": element_name + category,
|
||||
"CoordinateXAxis": point[0],
|
||||
"CoordinateYAxis": point[1],
|
||||
"CoordinateZAxis": point[2],
|
||||
}
|
||||
yield base_data | box_point_data
|
||||
|
||||
|
||||
def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str, None]:
|
||||
for unit in ifc_file.by_type("IfcUnitAssignment")[0].Units:
|
||||
if unit.is_a("IfcNamedUnit") and unit.UnitType == unit_type:
|
||||
@@ -937,6 +1008,7 @@ def get_facility_parent(
|
||||
|
||||
|
||||
def val(x: Any) -> Any:
|
||||
"""Replace n/a and empty strings with `None`."""
|
||||
return x if x not in ("", "n/a") else None
|
||||
|
||||
|
||||
@@ -1069,6 +1141,7 @@ config = {
|
||||
"bool_false": "No",
|
||||
"categories": {
|
||||
"Contact": {
|
||||
"colour": "r",
|
||||
"keys": ["Email"],
|
||||
"headers": [
|
||||
"Email",
|
||||
@@ -1097,6 +1170,7 @@ config = {
|
||||
"get_element_data": get_contact_data,
|
||||
},
|
||||
"Facility": {
|
||||
"colour": "r",
|
||||
"keys": ["Name"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1128,6 +1202,7 @@ config = {
|
||||
"get_element_data": get_facility_data,
|
||||
},
|
||||
"Floor": {
|
||||
"colour": "r",
|
||||
"keys": ["Name"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1147,6 +1222,7 @@ config = {
|
||||
"get_element_data": get_floor_data,
|
||||
},
|
||||
"Space": {
|
||||
"colour": "r",
|
||||
"keys": ["Name"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1169,6 +1245,7 @@ config = {
|
||||
"get_element_data": get_space_data,
|
||||
},
|
||||
"Zone": {
|
||||
"colour": "o",
|
||||
"keys": ["Name", "Category", "SpaceNames"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1187,6 +1264,7 @@ config = {
|
||||
"get_element_data": get_zone_data,
|
||||
},
|
||||
"Type": {
|
||||
"colour": "r",
|
||||
"keys": ["Name"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1231,6 +1309,7 @@ config = {
|
||||
"get_element_data": get_type_data,
|
||||
},
|
||||
"Component": {
|
||||
"colour": "r",
|
||||
"keys": ["Name"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1259,6 +1338,7 @@ config = {
|
||||
"get_element_data": get_component_data,
|
||||
},
|
||||
"System": {
|
||||
"colour": "o",
|
||||
"keys": ["Name", "Category", "ComponentNames"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1277,6 +1357,7 @@ config = {
|
||||
"get_element_data": get_system_data,
|
||||
},
|
||||
"Assembly": { # Note that this is technically "not required"
|
||||
"colour": "o",
|
||||
"keys": ["Name", "SheetName", "ParentName"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1297,6 +1378,7 @@ config = {
|
||||
"get_element_data": get_assembly_data,
|
||||
},
|
||||
"Connection": { # Note that this is technically "not required"
|
||||
"colour": "o",
|
||||
"keys": ["Name", "ConnectionType", "RowName1", "RowName2"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1320,6 +1402,7 @@ config = {
|
||||
"get_element_data": get_connection_data,
|
||||
},
|
||||
"Spare": {
|
||||
"colour": "r",
|
||||
"keys": ["Name"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1341,6 +1424,7 @@ config = {
|
||||
"get_element_data": get_spare_data,
|
||||
},
|
||||
"Resource": {
|
||||
"colour": "r",
|
||||
"keys": ["Name"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1358,6 +1442,7 @@ config = {
|
||||
"get_element_data": get_resource_data,
|
||||
},
|
||||
"Job": {
|
||||
"colour": "r",
|
||||
"keys": ["Name", "TypeName", "TaskNumber"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1386,6 +1471,7 @@ config = {
|
||||
"get_element_data": get_job_data,
|
||||
},
|
||||
"Document": {
|
||||
"colour": "r",
|
||||
"keys": ["Name", "Stage", "SheetName", "RowName"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1410,6 +1496,7 @@ config = {
|
||||
"get_element_data": get_document_data,
|
||||
},
|
||||
"Attribute": {
|
||||
"colour": "o",
|
||||
"keys": ["Name", "SheetName", "RowName"],
|
||||
"headers": [
|
||||
"Name",
|
||||
@@ -1429,7 +1516,32 @@ config = {
|
||||
"colours": "ririiirreeeoo",
|
||||
"sort": [{"name": "Category", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
|
||||
"get_category_elements": get_attributes,
|
||||
"get_element_data": get_attribute_data,
|
||||
"get_element_data": pass_category_elements_as_data,
|
||||
},
|
||||
"Coordinate": {
|
||||
"colour": "o",
|
||||
"keys": ["Name", "SheetName", "RowName"],
|
||||
"headers": (
|
||||
"Name",
|
||||
"CreatedBy",
|
||||
"CreatedOn",
|
||||
"Category",
|
||||
"SheetName",
|
||||
"RowName",
|
||||
"CoordinateXAxis",
|
||||
"CoordinateYAxis",
|
||||
"CoordinateZAxis",
|
||||
"ExtSystem",
|
||||
"ExtObject",
|
||||
"ExtIdentifier",
|
||||
"ClockwiseRotation",
|
||||
"ElevationalRotation",
|
||||
"YawRotation",
|
||||
),
|
||||
"colours": "ririiirrreeerrr",
|
||||
"sort": [{"name": "Category", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
|
||||
"get_category_elements": get_coordinate_elements,
|
||||
"get_element_data": pass_category_elements_as_data,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,7 +130,15 @@ class Usecase:
|
||||
continue
|
||||
elif obj.is_a("IfcFeatureElement"):
|
||||
# Feature elements affect the geometry of their parent, and
|
||||
# so logically should always move with the parent.
|
||||
# so logically should always move with the parent. However,
|
||||
# subchildren shouldn't move.
|
||||
placement2 = obj.ObjectPlacement
|
||||
for referenced_placement2 in placement2.ReferencedByPlacements:
|
||||
matrix2 = ifcopenshell.util.placement.get_local_placement(referenced_placement2)
|
||||
for obj2 in referenced_placement2.PlacesObject:
|
||||
results.append(
|
||||
{"product": obj2, "matrix": matrix2, "is_si": False, "should_transform_children": True}
|
||||
)
|
||||
continue
|
||||
results.append({"product": obj, "matrix": matrix, "is_si": False, "should_transform_children": True})
|
||||
return results
|
||||
|
||||
@@ -97,7 +97,6 @@ def create_owner_history(file: ifcopenshell.file) -> Union[ifcopenshell.entity_i
|
||||
# element.OwnerHistory = ifcopenshell.api.owner.create_owner_history(model)
|
||||
space = ifcopenshell.api.root.create_entity(model, ifc_class="IfcSpace")
|
||||
"""
|
||||
settings = {}
|
||||
|
||||
user = ifcopenshell.api.owner.settings.get_user(file)
|
||||
if file.schema != "IFC2X3" and not user:
|
||||
|
||||
@@ -74,7 +74,7 @@ SETTING = Literal[
|
||||
"no-wire-intersection-check",
|
||||
"no-wire-intersection-tolerance",
|
||||
"precision-factor",
|
||||
"debug",
|
||||
"debug-boolean",
|
||||
"boolean-attempt-2d",
|
||||
"weld-vertices",
|
||||
"use-world-coords",
|
||||
|
||||
@@ -763,37 +763,33 @@ class FacetTransformer(lark.Transformer):
|
||||
else:
|
||||
self.base_elements = elements.copy()
|
||||
self.elements = set()
|
||||
self.has_additive_facet_in_current_list = False
|
||||
self.container_trees = {}
|
||||
|
||||
def add_default_elements(self):
|
||||
if self.has_additive_facet_in_current_list:
|
||||
return
|
||||
self.has_additive_facet_in_current_list = True
|
||||
if self.base_elements:
|
||||
self.elements.update(self.base_elements)
|
||||
else:
|
||||
self.elements.update(self.file.by_type("IfcProduct"))
|
||||
self.elements.update(self.file.by_type("IfcTypeProduct"))
|
||||
|
||||
def get_results(self) -> set[ifcopenshell.entity_instance]:
|
||||
results: set[ifcopenshell.entity_instance] = set()
|
||||
for r in self.results:
|
||||
results |= r
|
||||
return results
|
||||
|
||||
def transform(self, tree: lark.ParseTree):
|
||||
def has_elements_token(tree: lark.tree.ParseTree) -> bool:
|
||||
for child in tree.iter_subtrees_topdown():
|
||||
data = child.data
|
||||
# Tokens that add elements to filter.
|
||||
if data in ("entity", "instance"):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Only elements tokens can add elements to the filter.
|
||||
# Fallback to default elements if they are not provided.
|
||||
if self.base_elements is None and not has_elements_token(tree):
|
||||
self.elements.update(self.file.by_type("IfcProduct"))
|
||||
self.elements.update(self.file.by_type("IfcTypeProduct"))
|
||||
|
||||
return super().transform(tree)
|
||||
|
||||
def facet_list(self, args):
|
||||
if self.elements:
|
||||
self.results.append(self.elements)
|
||||
self.elements = set()
|
||||
self.has_additive_facet_in_current_list = False
|
||||
|
||||
def instance(self, args):
|
||||
self.has_additive_facet_in_current_list = True
|
||||
if self.base_elements is None:
|
||||
if args[0].data == "globalid":
|
||||
try:
|
||||
@@ -816,6 +812,7 @@ class FacetTransformer(lark.Transformer):
|
||||
}
|
||||
|
||||
def entity(self, args):
|
||||
self.has_additive_facet_in_current_list = True
|
||||
if self.base_elements is None:
|
||||
if args[0].data == "ifc_class":
|
||||
try:
|
||||
@@ -844,6 +841,7 @@ class FacetTransformer(lark.Transformer):
|
||||
element_value = getattr(element, name, None)
|
||||
return self.compare(element_value, comparison, value)
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def type(self, args):
|
||||
@@ -853,6 +851,7 @@ class FacetTransformer(lark.Transformer):
|
||||
element_value = getattr(ifcopenshell.util.element.get_type(element), "Name", None)
|
||||
return self.compare(element_value, comparison, value)
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def material(self, args):
|
||||
@@ -870,6 +869,7 @@ class FacetTransformer(lark.Transformer):
|
||||
return result if comparison == "=" else not result
|
||||
return self.compare(None, comparison, value)
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def property(self, args):
|
||||
@@ -899,6 +899,7 @@ class FacetTransformer(lark.Transformer):
|
||||
return self.compare(element_value, comparison, value)
|
||||
return self.compare(None, comparison, value)
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def classification(self, args):
|
||||
@@ -918,6 +919,7 @@ class FacetTransformer(lark.Transformer):
|
||||
return result if comparison == "=" else not result
|
||||
return self.compare(None, comparison, value)
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def location(self, args):
|
||||
@@ -936,6 +938,7 @@ class FacetTransformer(lark.Transformer):
|
||||
return result if comparison == "=" else not result
|
||||
return self.compare(None, comparison, value)
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def group(self, args):
|
||||
@@ -949,6 +952,7 @@ class FacetTransformer(lark.Transformer):
|
||||
result = True
|
||||
return result if comparison == "=" else not result
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def parent(self, args):
|
||||
@@ -989,6 +993,7 @@ class FacetTransformer(lark.Transformer):
|
||||
for parent in parents:
|
||||
children |= set(ifcopenshell.util.element.get_decomposition(parent))
|
||||
|
||||
self.add_default_elements()
|
||||
if comparison == "=":
|
||||
self.elements = self.elements & children
|
||||
else:
|
||||
@@ -1000,6 +1005,7 @@ class FacetTransformer(lark.Transformer):
|
||||
def filter_function(element: ifcopenshell.entity_instance) -> bool:
|
||||
return self.compare(get_element_value(element, keys), comparison, value)
|
||||
|
||||
self.add_default_elements()
|
||||
self.elements = set(filter(filter_function, self.elements))
|
||||
|
||||
def get_container_tree(self, container: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
|
||||
@@ -145,7 +145,7 @@ def get_shape_matrix(shape: ShapeElementType) -> MatrixType:
|
||||
:param shape: Shape output calculated by IfcOpenShell
|
||||
:return: A 4x4 numpy array representing the transformation matrix
|
||||
"""
|
||||
return np.array(shape.transformation.matrix).reshape((4, 4), order="F")
|
||||
return np.frombuffer(shape.transformation_buffer, "d").reshape((4, 4), order="F")
|
||||
|
||||
|
||||
def get_bbox_centroid(geometry: ShapeType) -> tuple[float, float, float]:
|
||||
@@ -171,7 +171,7 @@ def get_vert_centroid(geometry: ShapeType) -> tuple[float, float, float]:
|
||||
return np.mean(get_vertices(geometry), axis=0)
|
||||
|
||||
|
||||
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
|
||||
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry: ShapeType) -> npt.NDArray[np.float64]:
|
||||
"""Calculates the element's bounding box centroid
|
||||
|
||||
The centroid is in global coordinates. Note that if you have the shape, it
|
||||
@@ -188,7 +188,7 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -
|
||||
return (mat @ np.array([*centroid, 1.0]))[0:3]
|
||||
|
||||
|
||||
def get_shape_bbox_centroid(shape: ShapeType, geometry: ShapeType) -> npt.NDArray[np.float64]:
|
||||
def get_shape_bbox_centroid(shape: ShapeElementType, geometry: ShapeType) -> npt.NDArray[np.float64]:
|
||||
"""Calculates the shape's bounding box centroid
|
||||
|
||||
The centroid is in global coordinates. Note that if you do not have the
|
||||
@@ -308,7 +308,7 @@ def get_edges_representation_item_ids(geometry: ShapeType) -> npt.NDArray[np.int
|
||||
return np.frombuffer(geometry.edges_item_ids_buffer, dtype="i")
|
||||
|
||||
|
||||
def get_shape_vertices(shape: ShapeType, geometry: ShapeType) -> npt.NDArray[np.float64]:
|
||||
def get_shape_vertices(shape: ShapeElementType, geometry: ShapeType) -> npt.NDArray[np.float64]:
|
||||
"""Get the shape's vertices as a numpy array
|
||||
|
||||
Vertices are in global coordinates. If you do not have the shape, you can
|
||||
@@ -417,7 +417,7 @@ def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry: S
|
||||
return max([v[2] for v in get_element_vertices(element, geometry)])
|
||||
|
||||
|
||||
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
||||
def get_bbox(vertices: npt.NDArray[np.float64]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
||||
"""Gets the bounding box of vertices
|
||||
|
||||
:param vertices: An iterable of vertices
|
||||
@@ -426,16 +426,7 @@ def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[npt.NDArray[np.float64], np
|
||||
right. E.g. (np.array([minx, miny, minz]), np.array([maxx, maxy,
|
||||
maxz]))
|
||||
"""
|
||||
x_values = [v[0] for v in vertices]
|
||||
y_values = [v[1] for v in vertices]
|
||||
z_values = [v[2] for v in vertices]
|
||||
minx = min(x_values)
|
||||
maxx = max(x_values)
|
||||
miny = min(y_values)
|
||||
maxy = max(y_values)
|
||||
minz = min(z_values)
|
||||
maxz = max(z_values)
|
||||
return (np.array([minx, miny, minz]), np.array([maxx, maxy, maxz]))
|
||||
return (np.min(vertices, axis=0), np.max(vertices, axis=0))
|
||||
|
||||
|
||||
def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32]) -> float:
|
||||
|
||||
@@ -90,6 +90,14 @@ def np_normalized(v: VectorType) -> np.ndarray:
|
||||
return np.divide(v, np.linalg.norm(v))
|
||||
|
||||
|
||||
def np_matrix_normalized(matrix: np.ndarray) -> np.ndarray:
|
||||
# Ensure translation is not affected.
|
||||
scale_factors = np.linalg.norm(matrix[:3, :3], axis=0)
|
||||
rotation_matrix = matrix.copy()
|
||||
rotation_matrix[:3, :3] /= scale_factors
|
||||
return rotation_matrix
|
||||
|
||||
|
||||
def np_lerp(a: VectorType, b: VectorType, t: float) -> np.ndarray:
|
||||
return a + np.subtract(b, a) * t
|
||||
|
||||
@@ -178,6 +186,23 @@ def np_rotation_matrix(
|
||||
return matrix
|
||||
|
||||
|
||||
def np_matrix_to_euler(matrix: np.ndarray) -> tuple[float, float, float]:
|
||||
"""Convert a rotation matrix to Euler angles.
|
||||
|
||||
Designed to work similar to `mathutils.Matrix.to_euler`.
|
||||
Currently only XYZ rotation is supported.
|
||||
"""
|
||||
if matrix.shape not in ((3, 3), (4, 4)):
|
||||
raise ValueError(f"Matrix must be 3x3 or 4x4, got {matrix.shape}.")
|
||||
|
||||
matrix = np_matrix_normalized(matrix)
|
||||
y = -np.arcsin(matrix[2, 0])
|
||||
cos_y = np.cos(y)
|
||||
x = np.arctan2(matrix[2, 1] / cos_y, matrix[2, 2] / cos_y)
|
||||
z = np.arctan2(matrix[1, 0] / cos_y, matrix[0, 0] / cos_y)
|
||||
return (x, y, z)
|
||||
|
||||
|
||||
def np_normal(vectors: SequenceOfVectors) -> np.ndarray:
|
||||
"""Normal of 3D Polygon.
|
||||
|
||||
|
||||
@@ -545,11 +545,12 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
|
||||
with pytest.raises(RuntimeError):
|
||||
self.file.by_id(previous_placement_id)
|
||||
|
||||
def test_changing_placements_always_affecting_child_features_as_a_special_case(self):
|
||||
def test_changing_placements_always_affecting_child_features_but_not_subchildren_as_a_special_case(self):
|
||||
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||
ifcopenshell.api.unit.assign_unit(self.file)
|
||||
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
subelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcOpeningElement")
|
||||
subsubelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoor")
|
||||
|
||||
matrix = numpy.eye(4)
|
||||
matrix[:3, 3] = (1, 1, 1)
|
||||
@@ -557,16 +558,23 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
|
||||
submatrix = numpy.eye(4)
|
||||
submatrix[:3, 3] = (1, 2, 3)
|
||||
|
||||
subsubmatrix = numpy.eye(4)
|
||||
subsubmatrix[:3, 3] = (7, 8, 9)
|
||||
|
||||
shifted_submatrix = numpy.eye(4)
|
||||
shifted_submatrix[:3, 3] = (1, 3, 5)
|
||||
|
||||
ifcopenshell.api.void.add_opening(self.file, opening=subelement, element=element)
|
||||
ifcopenshell.api.void.add_filling(self.file, opening=subelement, element=subsubelement)
|
||||
previous_placement_id = ifcopenshell.api.geometry.edit_object_placement(
|
||||
self.file, product=element, matrix=matrix.copy(), is_si=False
|
||||
).id()
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
self.file, product=subelement, matrix=submatrix.copy(), is_si=False
|
||||
)
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
self.file, product=subsubelement, matrix=subsubmatrix.copy(), is_si=False
|
||||
)
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
self.file,
|
||||
product=element,
|
||||
@@ -578,7 +586,11 @@ class TestEditObjectPlacement(test.bootstrap.IFC4):
|
||||
assert numpy.array_equal(
|
||||
ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), shifted_submatrix
|
||||
)
|
||||
assert numpy.array_equal(
|
||||
ifcopenshell.util.placement.get_local_placement(subsubelement.ObjectPlacement), subsubmatrix
|
||||
)
|
||||
assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement
|
||||
assert subsubelement.ObjectPlacement.PlacementRelTo == subelement.ObjectPlacement
|
||||
# old placement should be removed to avoid orphaned entities
|
||||
with pytest.raises(RuntimeError):
|
||||
self.file.by_id(previous_placement_id)
|
||||
|
||||
@@ -30,6 +30,7 @@ from ifcopenshell.util.shape_builder import (
|
||||
np_angle_signed,
|
||||
np_normal,
|
||||
np_intersect_line_line,
|
||||
np_matrix_to_euler,
|
||||
)
|
||||
from math import degrees, radians
|
||||
from typing import Any, Union
|
||||
@@ -57,6 +58,21 @@ class TestMathutilsCompatibleMethods(test.bootstrap.IFC4):
|
||||
rotation_vector_args = radians(45), 4, Vector((1, 1, 1)).normalized()
|
||||
assert np.allclose(Matrix.Rotation(*rotation_vector_args), np_rotation_matrix(*rotation_vector_args))
|
||||
|
||||
def test_np_matrix_to_euler(self):
|
||||
from mathutils import Euler
|
||||
|
||||
# Test 3x3.
|
||||
rot = Euler((0.5, 0.5, 0.5)).to_matrix()
|
||||
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
|
||||
|
||||
rot = rot.to_4x4()
|
||||
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
|
||||
|
||||
# Ensure support scaled matrices.
|
||||
rot = Euler((0.5, 0.5, 0.5)).to_matrix()
|
||||
rot.col[0] *= 2
|
||||
assert np.allclose(rot.to_euler(), np_matrix_to_euler(V(rot)))
|
||||
|
||||
def test_np_angle(self):
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
@@ -502,9 +502,7 @@ Token IfcParse::GeneralTokenPtr(IfcSpfLexer* lexer, unsigned start, unsigned end
|
||||
if (first == '#') {
|
||||
token.type = Token_IDENTIFIER;
|
||||
if (!ParseInt(tokenStr.c_str() + 1, token.value_int)) {
|
||||
Logger::Message(Logger::LOG_ERROR, "Token '" + tokenStr + "' at offset " + std::to_string(token.startPos) + " is not valid");
|
||||
token.type = Token_OPERATOR;
|
||||
token.value_char = '$';
|
||||
throw IfcException("Identifier token as not integer");
|
||||
}
|
||||
} else if (first == '\'') {
|
||||
token.type = Token_STRING;
|
||||
|
||||
@@ -268,7 +268,7 @@ class Context:
|
||||
|
||||
opening.Representation = self.model.createIfcProductDefinitionShape(
|
||||
Representations=[
|
||||
self.model.createIfcRepresentation(
|
||||
self.model.createIfcShapeRepresentation(
|
||||
body,
|
||||
"Body",
|
||||
"SweptSolid",
|
||||
|
||||
Reference in New Issue
Block a user