Compare commits

..

27 Commits

Author SHA1 Message Date
Dion Moult efede9eb03 No idea why Github actions is failing for MacOS. All code looks good to me. 2025-01-14 00:14:03 +11:00
Andrej730 ac315e8a55 ifcfm cobie 2.4 - color code excel tabs
example - https://i.imgur.com/tSeqyjj.png
2025-01-13 17:53:02 +05:00
Andrej730 b7c18221f4 get_bbox - optimize by using numpy arrays 2025-01-13 17:53:02 +05:00
Andrej730 7a49f1611e get_shape_matrix - small optimization
reuse existing matrix instead of recreating new
2025-01-13 17:53:02 +05:00
Andrej730 5c0a7af830 cobie24 - implement Coordinate tab #5926 2025-01-13 17:53:02 +05:00
Andrej730 a968b61d02 np_matrix_to_euler, np_matrix_normalized 2025-01-13 17:53:01 +05:00
Andrej730 0ac9d739f8 remove unused code 2025-01-13 17:53:01 +05:00
Andrej730 74c6480808 bim.add_element to select added object 2025-01-13 17:53:01 +05:00
Andrej730 0601802005 typing 2025-01-13 17:53:01 +05:00
Dion Moult 9784fb1c49 Pyodide example app should use IfcShapeRepresentation to be valid 2025-01-13 23:12:49 +11:00
Gorgious56 039dcbaefa Stair Modifier : You can now set a total length target that will update individual tread runs automatically. You can lock the total length target and modify tread run or number of treads to keep the same global dimensions. 2025-01-13 12:04:13 +01:00
Gorgious56 1cd17fd5f8 Merge pull request #5947 from IfcOpenShell/update_type_manager
Update Type Manager
2025-01-13 09:40:42 +01:00
Gorgious56 2f0f9fe08b Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into update_type_manager 2025-01-13 09:38:41 +01:00
Gorgious56 50ed75fcc2 Fix potential bug where a class doesn't have a predefined type 2025-01-13 09:32:23 +01:00
Gorgious56 9eaf41c9f0 The filter search field now also filters description and predefined type 2025-01-13 09:28:56 +01:00
Dion Moult 581a674654 Fix #5952. Aggressively block scaling of IFC elements to prevent user confusion. 2025-01-13 18:01:25 +11:00
Dion Moult e29f686d3f Update setting literal from old name of "debug" to new name "debug-boolean". 2025-01-13 15:45:27 +11:00
Dion Moult 8cf7057abc Fix #5954. Special automatic movement of features shouldn't then also move children of those features.
If you move a wall, and that wall has features (e.g. openings), it's
desirable to also move those openings (because they are invisible). This
is a special exception to `should_transform_children` because the
definition of the feature (opening) is inherently tied to the parent
(wall).

What wasn't considered is that this would typically then also move
subchildren of the features (e.g. fills like doors). I'm surprised
nobody caught this earlier.

I did also consider another approach where if you move a wall, it moves
all unfilled openings, and if you move a door which fills a opening, it
moves the opening too. Intuitively it sounds nice, but it doesn't work
because:

 - Openings can have multiple fillings. If you move all fillings, they
all fight to move the openings.
 - All logic about children goes one way: a placement may have child
placements relative to it. This breaks the convention (if moving a door
instead moves its opening) which can make brains explode.
 - It starts to conflate rules about relative / referenced placements
with spatial decomposition. We assume all IFCs are valid and follows the
convention of relative placement but we cannot guarantee this. This also
leads to brain explosion.
2025-01-13 15:44:52 +11:00
Dion Moult 316b9554d4 Fix #5959. Bug where default selection basket in selector query was not added per facet list.
Previously the code looked for additive facets (entity, instance)
anywhere in the query, and then only added the default basket once at
the beginning of the query. This change makes it look for additive
facets in order (that way you could start with a default selection, and
then later use an additive facet) and does it per facet list (so you can
omit the additive facet at the beginning of each list).
2025-01-13 12:46:44 +11:00
tim 95fa6f39c4 New furniture icon
Chair icon for furniture
2025-01-13 10:47:35 +11:00
Thomas Krijnen 65ffbe081b Pyodide app readme 2025-01-12 15:50:19 +01:00
Gorgious56 68f175fc71 Fix bug when trying to add an ifc type occurence from the toolbar in the properties editor 2025-01-10 15:42:37 +01:00
Gorgious56 a428169a13 Fix bug when duplicating an ifc type 2025-01-10 15:29:31 +01:00
Gorgious56 7db0196791 Fix bug where the thumbnail of the active class wasn't updated 2025-01-10 15:17:26 +01:00
Gorgious56 4f4c557f61 Another attempt.
Went back to previous toolbar : icon + type name + dot grid icon which all launch the type manager.
    Validating the type manager popup now only closes it. Functionally is the same thing as clicking Cancel. Don't really know what to do to fix it. Does cancel make sense in the type manager ? Do we want to be able to undo what is possible to be done from within the type manager ?
    You can change the active type either with the dropdown in the popup and in the grid flow. I changed it so that changing the type in the dropdown jumps to the corresponding page. BTW first time using github copilot to figure out the maths and it worked magically :) . I'm not against removing the dropdown but I feel like it's nice to see a plain list to choose from.
    You can input a specific page number or scrub the page number field. Might be usefull if user has dozens of pages to flip through.
    Right now the search is very simple, it does not implement fuzzy search or anything fancy with *
2025-01-10 14:24:33 +01:00
Gorgious56 aafb6fc426 Add search bar with simple filter function to the type manager 2025-01-10 09:44:12 +01:00
Gorgious56 a6e953a550 Update Type Manager
Add back the type dropdown and looking glass icon in the toolbar
Clicking on a type in the manager no longer closes the popup anymore.
A visual indictor is displayed on the active type
Changed the parameters icon in the individual type interface

Fix bug in "Slab from Walls" Shift + A code
2025-01-09 17:45:40 +01:00
24 changed files with 464 additions and 195 deletions
+6 -1
View File
@@ -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.
-16
View File
@@ -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)
+42 -29
View File
@@ -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()
+60 -3
View File
@@ -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)
+44 -26
View File
@@ -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"]
+62 -63
View File
@@ -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
+1
View File
@@ -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):
-2
View File
@@ -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:
+9 -5
View File
@@ -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
View File
@@ -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
+1 -3
View File
@@ -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;
+1 -1
View File
@@ -268,7 +268,7 @@ class Context:
opening.Representation = self.model.createIfcProductDefinitionShape(
Representations=[
self.model.createIfcRepresentation(
self.model.createIfcShapeRepresentation(
body,
"Body",
"SweptSolid",