mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
See #1227. Reimplement extend walls to slab (or anything with negative Z faces)
There are a few differences to the previous implementation: 1. It is no longer recalculated on wall regeneration. The new wall regeneration is strict to the spec on the rules of rel connects path, and so this being a "userdefined" connection we only calculate it explicitly when the user invokes the operator. 2. It uses meshes instead of clipping planes, so you can clip to strange shapes or gable roofs or whatever. Nice.
This commit is contained in:
@@ -1241,10 +1241,17 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# Extend LAYER2s to LAYER3
|
||||
[o.select_set(False) for o in selected_usages.get("PROFILE", [])]
|
||||
[o.select_set(False) for o in selected_usages.get("LAYER3", []) if o != bpy.context.active_object]
|
||||
try:
|
||||
core.join_walls_TZ(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
|
||||
except core.RequireAtLeastTwoLayeredElements as e:
|
||||
self.report({"ERROR"}, str(e))
|
||||
slab = None
|
||||
walls = []
|
||||
if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER3":
|
||||
slab = obj
|
||||
for obj in tool.Blender.get_selected_objects(include_active=False):
|
||||
if (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
walls.append(obj)
|
||||
if slab and walls:
|
||||
core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls)
|
||||
else:
|
||||
self.report({"ERROR"}, "Please select at least one LAYER2 element and an active LAYER3 element")
|
||||
|
||||
elif self.active_material_usage == "LAYER2":
|
||||
# Extend LAYER2s to LAYER2
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.core.tool as tool
|
||||
from typing import Literal
|
||||
from typing import Literal, Iterable
|
||||
|
||||
|
||||
def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None:
|
||||
@@ -63,6 +63,21 @@ def join_walls_LV(
|
||||
joiner.connect(another_selected_object, active_obj)
|
||||
|
||||
|
||||
def extend_wall_to_slab(
|
||||
ifc: tool.Ifc, geometry: tool.Geometry, model: tool.Model, slab_obj, wall_objs: Iterable
|
||||
) -> None:
|
||||
if not (clip := model.get_slab_clipping_bmesh(slab_obj)):
|
||||
return # Nothing to clip?
|
||||
slab = ifc.get_entity(slab_obj)
|
||||
for obj in wall_objs:
|
||||
if ifc.is_moved(obj):
|
||||
geometry.run_edit_object_placement(obj=obj)
|
||||
wall = ifc.get_entity(obj)
|
||||
model.clip_wall_to_slab(wall, clip)
|
||||
model.connect_wall_to_slab(wall, slab)
|
||||
model.reload_body_representation(wall_objs)
|
||||
|
||||
|
||||
def join_walls_TZ(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None:
|
||||
selected_objs = [
|
||||
o
|
||||
|
||||
@@ -432,6 +432,7 @@ class Geometry:
|
||||
def rename_object(cls, obj, name): pass
|
||||
def replace_object_data_globally(cls, old_data, new_data): pass
|
||||
def resolve_mapped_representation(cls, representation): pass
|
||||
def run_edit_object_placement(cls, obj=None): pass
|
||||
def run_geometry_update_representation(cls, obj=None): pass
|
||||
def run_style_add_style(cls, obj=None): pass
|
||||
def select_connection(cls, connection): pass
|
||||
@@ -562,22 +563,26 @@ class Misc:
|
||||
|
||||
@interface
|
||||
class Model:
|
||||
def clip_wall_to_slab(cls, element, bm): pass
|
||||
def connect_wall_to_slab(cls, wall, slab): pass
|
||||
def convert_si_to_unit(cls, value): pass
|
||||
def convert_unit_to_si(cls, value): pass
|
||||
def export_points(cls, position, indices): pass
|
||||
def export_profile(cls, obj, position=None): pass
|
||||
def generate_occurrence_name(cls, element_type, ifc_class): pass
|
||||
def get_extrusion(cls, representation): pass
|
||||
def import_profile(cls, profile, obj=None, position=None): pass
|
||||
def get_manual_booleans(cls, element): pass
|
||||
def get_material_layer_parameters(cls, element): pass
|
||||
def get_slab_clipping_bmesh(cls, obj): pass
|
||||
def get_usage_type(cls, element): pass
|
||||
def get_wall_axis(cls, obj, layers=None): pass
|
||||
def import_curve(cls, curve, obj=None, position=None): pass
|
||||
def import_profile(cls, profile, obj=None, position=None): pass
|
||||
def import_rectangle(cls, obj, position, profile): pass
|
||||
def load_openings(cls, openings): pass
|
||||
def purge_scene_openings(cls): pass
|
||||
def get_usage_type(cls, element): pass
|
||||
def get_material_layer_parameters(cls, element): pass
|
||||
def get_manual_booleans(cls, element): pass
|
||||
def get_wall_axis(cls, obj, layers=None): pass
|
||||
def regenerate_array(cls, parent, data): pass
|
||||
def reload_body_representation(cls, obj_or_objects): pass
|
||||
def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass
|
||||
|
||||
|
||||
|
||||
@@ -154,15 +154,22 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
@classmethod
|
||||
def get_active_object(cls, is_selected: bool = False) -> Union[bpy.types.Object, None]:
|
||||
obj = getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active
|
||||
if not is_selected:
|
||||
return obj
|
||||
if obj in cls.get_selected_objects(include_active=False):
|
||||
return obj
|
||||
"""Gets the active object
|
||||
|
||||
:param is_selected: If true, the active object also needs to be selected.
|
||||
"""
|
||||
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
|
||||
if not is_selected:
|
||||
return obj
|
||||
if obj.select_get():
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_selected_objects(cls, include_active: bool = True) -> set[bpy.types.Object]:
|
||||
"""Get selected objects including active object."""
|
||||
"""Get selected objects
|
||||
|
||||
:param include_active: If true, the active object is included regardless if it is also selected.
|
||||
"""
|
||||
if selected_objects := getattr(bpy.context, "selected_objects", None):
|
||||
if include_active and (active_obj := cls.get_active_object()):
|
||||
return set(selected_objects + [active_obj])
|
||||
|
||||
@@ -63,6 +63,16 @@ class Debug(bonsai.core.tool.Debug):
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def debug_bmesh(
|
||||
cls, bm: bpy.types.BMesh, name: str = "Debug"
|
||||
) -> bpy.types.Object:
|
||||
mesh = bpy.data.meshes.new("Debug")
|
||||
bm.to_mesh(mesh)
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def debug_geometry(
|
||||
cls, verts: list[Vector] = [], edges: list[tuple[int, int]] = [], name: str = "Debug"
|
||||
|
||||
@@ -1986,3 +1986,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
||||
bm.transform(obj.matrix_world)
|
||||
return BVHTree.FromBMesh(bm)
|
||||
|
||||
@classmethod
|
||||
def run_edit_object_placement(cls, obj: bpy.types.Object) -> None:
|
||||
return bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
@@ -39,7 +39,7 @@ import bonsai.core.geometry
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.geometry as geometry
|
||||
from math import atan, cos, degrees, radians, pi
|
||||
from math import atan, cos, degrees, pi, inf
|
||||
from mathutils import Matrix, Vector
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
@@ -284,7 +284,7 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
@classmethod
|
||||
def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""return first found IfcExtrudedAreaSolid"""
|
||||
"""Return first found IfcExtrudedAreaSolid"""
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
@@ -2101,3 +2101,83 @@ class Model(bonsai.core.tool.Model):
|
||||
return
|
||||
op = layout.operator("bim.material_ui_select", icon="ZOOM_SELECTED", text="")
|
||||
op.material_id = material_id_int
|
||||
|
||||
@classmethod
|
||||
def get_slab_clipping_bmesh(cls, obj: bpy.types.Object) -> bpy.types.BMesh | None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
clipping_bm = bmesh.new()
|
||||
vertex_map = {}
|
||||
|
||||
for face in bm.faces:
|
||||
face.normal_update()
|
||||
normal = face.normal.to_4d()
|
||||
normal.w = 0
|
||||
if (obj.matrix_world @ normal).z >= 0:
|
||||
continue
|
||||
new_verts = []
|
||||
for vert in face.verts:
|
||||
if not (new_vert := vertex_map.get(vert.index, None)):
|
||||
new_vert = clipping_bm.verts.new(obj.matrix_world @ vert.co / unit_scale)
|
||||
vertex_map[vert.index] = new_vert
|
||||
new_verts.append(new_vert)
|
||||
clipping_bm.faces.new(new_verts)
|
||||
|
||||
if not len(clipping_bm.faces):
|
||||
return
|
||||
|
||||
return clipping_bm # clipping_bm is in project units
|
||||
|
||||
@classmethod
|
||||
def clip_wall_to_slab(cls, wall: ifcopenshell.entity_instance, clipping_bm: bpy.types.BMesh) -> None:
|
||||
matrix_i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall.ObjectPlacement))
|
||||
bm = clipping_bm.copy()
|
||||
bmesh.ops.transform(bm, matrix=Matrix(matrix_i.tolist()), verts=bm.verts)
|
||||
|
||||
bm.verts.ensure_lookup_table()
|
||||
zs = [v.co.z for v in bm.verts]
|
||||
min_z = min(zs)
|
||||
max_z = max(zs)
|
||||
|
||||
operand = None
|
||||
if (z := max_z - min_z) and not np.isclose(z, 0.0):
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
|
||||
result = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
extruded_verts = [elem for elem in result["geom"] if isinstance(elem, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, verts=extruded_verts, vec=(0, 0, z))
|
||||
|
||||
verts = [v.co for v in bm.verts]
|
||||
faces = [[v.index for v in p.verts] for p in bm.faces]
|
||||
operand = builder.mesh(verts, faces)
|
||||
|
||||
for extrusion in ifcopenshell.util.shape.get_base_extrusions(wall) or []:
|
||||
if extrusion.Position:
|
||||
position = ifcopenshell.util.placement.get_axis2placement(extrusion.Position)
|
||||
else:
|
||||
position = np.eye(4)
|
||||
|
||||
direction = np.array(extrusion.ExtrudedDirection[0])
|
||||
direction /= np.linalg.norm(direction)
|
||||
direction = position @ np.append(direction, 0.0)
|
||||
|
||||
if direction[2] <= 0 or position[2][3] > max_z:
|
||||
continue
|
||||
|
||||
extrusion.Depth = max_z / direction[2]
|
||||
|
||||
if operand:
|
||||
booleans = ifcopenshell.api.geometry.add_boolean(
|
||||
tool.Ifc.get(), first_item=extrusion, second_items=[operand]
|
||||
)
|
||||
tool.Model.mark_manual_booleans(wall, booleans)
|
||||
|
||||
@classmethod
|
||||
def connect_wall_to_slab(cls, wall: ifcopenshell.entity_instance, slab: ifcopenshell.entity_instance) -> None:
|
||||
ifcopenshell.api.geometry.connect_element(
|
||||
tool.Ifc.get(), relating_element=slab, related_element=wall, description="TOP"
|
||||
)
|
||||
|
||||
@@ -721,6 +721,26 @@ def get_extrusions(element: ifcopenshell.entity_instance) -> Union[list[ifcopens
|
||||
return extrusions
|
||||
|
||||
|
||||
def get_base_extrusions(element: ifcopenshell.entity_instance) -> Union[list[ifcopenshell.entity_instance], None]:
|
||||
"""Gets all base extrusions used to define an element's model body geometry
|
||||
|
||||
A base extrusion is assumed to be an extrusion prior to all boolean
|
||||
results.
|
||||
|
||||
:param element: The element occurrence
|
||||
:return: A list of extrusion representation items or `None` if element has no representation.
|
||||
"""
|
||||
if not (rep := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")):
|
||||
return
|
||||
extrusions = []
|
||||
for item in ifcopenshell.util.representation.resolve_representation(rep).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
extrusions.append(item)
|
||||
return extrusions
|
||||
|
||||
|
||||
def get_total_edge_length(geometry: ShapeType) -> float:
|
||||
"""Calculates the total length of edges in a given geometry.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user