mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
Added ChangeSwingDirection operator for parametric doors
This commit is contained in:
committed by
Ryan Schultz
parent
d2aa5f8274
commit
07b6560835
@@ -84,6 +84,7 @@ classes = (
|
||||
product.MirrorElements,
|
||||
product.TrueMirrorElements,
|
||||
product.SetActiveType,
|
||||
product.ChangeSwingDirection,
|
||||
workspace.Hotkey,
|
||||
workspace.BIM_MT_add_representation_item,
|
||||
wall.AddPerpendicularWall,
|
||||
|
||||
@@ -682,494 +682,6 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
obj.matrix_world = newmat
|
||||
|
||||
class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.mirror_geometry"
|
||||
bl_label = "Mirror Element Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Mirrors the selected objects by mirroring their representation (or their types representation)"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
self.mirror_obj(obj)
|
||||
return { "FINISHED" }
|
||||
|
||||
def mirror_obj(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
type_element = ifcopenshell.util.element.get_type(element)
|
||||
|
||||
active_context = tool.Geometry.get_active_representation_context(obj)
|
||||
|
||||
if type_element and element.id() != type_element.id():
|
||||
# obj has a type, use / create inverted type and assign it
|
||||
self.assign_inverted_type(element)
|
||||
else:
|
||||
# invert representation of entity directly
|
||||
self.invert_representation(element)
|
||||
|
||||
# bonsai does not automatically switch to the representation that should be active in the given context
|
||||
# when switching to a type that was previously viewed in another context (e.g. plan view),
|
||||
# the wrong representation will be used.
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=ifcopenshell.util.representation.get_representation(element, active_context),
|
||||
should_reload=False,
|
||||
is_global=False,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
def invert_general_object(self, element):
|
||||
if element.is_a("IfcProduct"):
|
||||
if not element.Representation:
|
||||
return
|
||||
|
||||
for representation in element.Representation.Representations:
|
||||
for item in representation.Items:
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
builder.mirror(item, (1, 0), create_copy=False)
|
||||
elif element.is_a("IfcTypeProduct"):
|
||||
for representation_map in element.RepresentationMaps:
|
||||
for item in representation_map.MappedRepresentation.Items:
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
builder.mirror(item, (1, 0), create_copy=False)
|
||||
|
||||
tool.Geometry.reload_representation(tool.Ifc.get_object(element))
|
||||
|
||||
def invert_door_swing(self, element):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
|
||||
pset_data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
|
||||
|
||||
if "LEFT" in pset_data["door_type"]:
|
||||
pset_data["door_type"] = pset_data["door_type"].replace("LEFT", "RIGHT")
|
||||
elif "RIGHT" in pset_data["door_type"]:
|
||||
pset_data["door_type"] = pset_data["door_type"].replace("RIGHT", "LEFT")
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
pset_data_str = tool.Ifc.get().createIfcText(json.dumps(pset_data, default=list))
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": pset_data_str})
|
||||
|
||||
pset_data.update(pset_data.pop("lining_properties"))
|
||||
pset_data.update(pset_data.pop("panel_properties"))
|
||||
pset_data.update(tool.Model.get_constituents_props_data(element))
|
||||
|
||||
# we need this workaround because set_props_kwargs_from_ifc_data will
|
||||
# "update" the mesh of the active object, which will switch its representation
|
||||
prev_active = bpy.context.view_layer.objects.active
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
|
||||
props = tool.Model.get_door_props(obj)
|
||||
props.set_props_kwargs_from_ifc_data(pset_data)
|
||||
|
||||
bpy.context.view_layer.objects.active = prev_active
|
||||
|
||||
# regenerate door geometry
|
||||
update_door_modifier_representation(obj)
|
||||
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
|
||||
def invert_representation(self, element):
|
||||
if ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"):
|
||||
self.invert_door_swing(element)
|
||||
else:
|
||||
self.invert_general_object(element)
|
||||
|
||||
def assign_inverted_type(self, element):
|
||||
type_element = ifcopenshell.util.element.get_type(element)
|
||||
|
||||
inverted_type = tool.Blender.Modifier.has_mirrored_type(type_element)
|
||||
if not inverted_type:
|
||||
old_to_new, _ = tool.Geometry.duplicate_ifc_objects([ tool.Ifc.get_object(type_element) ])
|
||||
inverted_type = old_to_new[type_element][0]
|
||||
self.invert_representation(inverted_type)
|
||||
tool.Blender.Modifier.set_mirrored_type(inverted_type, type_element)
|
||||
tool.Blender.Modifier.set_mirrored_type(type_element, inverted_type)
|
||||
inverted_type.Name = f"{inverted_type.Name}.Mirror"
|
||||
|
||||
bonsai.core.type.assign_type(tool.Ifc, tool.Type, element, inverted_type)
|
||||
|
||||
|
||||
class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.mirror_geometry"
|
||||
bl_label = "Mirror Element Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Mirrors the selected objects by inverting their representation. "
|
||||
"If an active object is set, mirrors about its YZ plane; otherwise mirrors in place along the X axis. "
|
||||
"Shift: duplicate and mirror, leaving the original in place"
|
||||
)
|
||||
keep_original: bpy.props.BoolProperty(name="Keep Original", default=False)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
active_obj = context.active_object
|
||||
mirror_ref = None
|
||||
if active_obj:
|
||||
objs_to_mirror = [obj for obj in context.selected_objects if obj != active_obj]
|
||||
if objs_to_mirror:
|
||||
mirror_ref = active_obj
|
||||
if mirror_ref is None:
|
||||
objs_to_mirror = list(context.selected_objects)
|
||||
|
||||
if self.keep_original:
|
||||
if mirror_ref:
|
||||
mirror_ref.select_set(False)
|
||||
bpy.ops.bim.override_object_duplicate_move(is_interactive=False)
|
||||
objs_to_mirror = [obj for obj in context.selected_objects if obj != mirror_ref]
|
||||
if mirror_ref:
|
||||
mirror_ref.select_set(True)
|
||||
|
||||
for obj in objs_to_mirror:
|
||||
self.mirror_obj(context, obj, mirror_ref)
|
||||
return {"FINISHED"}
|
||||
|
||||
def mirror_obj(self, context: bpy.types.Context, obj: bpy.types.Object, mirror_ref: bpy.types.Object = None):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
type_element = ifcopenshell.util.element.get_type(element)
|
||||
|
||||
active_context = tool.Geometry.get_active_representation_context(obj)
|
||||
|
||||
bb_data = tool.Blender.get_object_bounding_box(obj)
|
||||
|
||||
# Compute the geometry inversion axis from the mirror reference's local X (= mirror plane normal).
|
||||
# Without a reference, always fall back to inverting along the object's local X.
|
||||
if mirror_ref:
|
||||
mirror_normal_world = mirror_ref.matrix_world.to_3x3().col[0].normalized()
|
||||
mirror_normal_local = obj.matrix_world.to_3x3().inverted() @ mirror_normal_world
|
||||
mirror_axes = (
|
||||
1.0 if abs(mirror_normal_local.x) > 0.5 else 0.0,
|
||||
1.0 if abs(mirror_normal_local.y) > 0.5 else 0.0,
|
||||
1.0 if abs(mirror_normal_local.z) > 0.5 else 0.0,
|
||||
)
|
||||
else:
|
||||
mirror_axes = (1, 0, 0)
|
||||
|
||||
# Snapshot opening world placements AND slab placement BEFORE any geometry or origin changes.
|
||||
# We work in the slab's LOCAL coordinate space so the mirrored relative offset is correct
|
||||
# regardless of when (or whether) the slab's own IFC ObjectPlacement gets synced by the depsgraph.
|
||||
opening_placements_before: dict[int, np.ndarray] = {}
|
||||
M_slab_before: np.ndarray | None = None
|
||||
if hasattr(element, "HasOpenings") and element.HasOpenings:
|
||||
M_slab_before = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement).copy()
|
||||
for rel in element.HasOpenings:
|
||||
opening = rel.RelatedOpeningElement
|
||||
M = ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement)
|
||||
opening_placements_before[opening.id()] = M.copy()
|
||||
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
type_has_reps = bool(type_element and (type_element.RepresentationMaps or []))
|
||||
# LAYER2 (walls) and LAYER3 (slabs) generate instance-specific bodies via DumbWallGenerator /
|
||||
# DumbSlabGenerator rather than mapping the type's RepresentationMaps. assign_inverted_type
|
||||
# only flips the *type* geometry and would leave the instance body unchanged, so both layer
|
||||
# usage types must go through invert_representation instead.
|
||||
is_assign_type_path = bool(type_element and element.id() != type_element.id() and usage_type not in ("LAYER2", "LAYER3") and type_has_reps)
|
||||
if is_assign_type_path:
|
||||
self.assign_inverted_type(element, mirror_axes)
|
||||
else:
|
||||
# invert representation of entity directly;
|
||||
# LAYER2/LAYER3 (walls/slabs) and elements whose type carries no geometry use this path
|
||||
# Update opening placements BEFORE invert_representation so its internal reload
|
||||
# sees the correct positions. No element rotation change on this path → frame_change = I.
|
||||
if opening_placements_before and M_slab_before is not None:
|
||||
self._apply_opening_mirror(element, mirror_axes, M_slab_before, opening_placements_before, np.eye(3))
|
||||
self.invert_representation(element, mirror_axes)
|
||||
# For LAYER2 walls, layers stack along local Y (LayerSetDirection=AXIS2). A Y-axis flip
|
||||
# reverses that direction, so DirectionSense and OffsetFromReferenceLine must both invert.
|
||||
# (X/Z flips don't touch local Y, and the assign_inverted_type path handles Y-mirror via
|
||||
# a 180°Z element rotation which implicitly flips local Y without changing the IFC attribute.)
|
||||
if usage_type == "LAYER2" and mirror_axes[1] > 0.5:
|
||||
mat_usage = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
if mat_usage and mat_usage.is_a("IfcMaterialLayerSetUsage"):
|
||||
mat_usage.DirectionSense = "NEGATIVE" if mat_usage.DirectionSense == "POSITIVE" else "POSITIVE"
|
||||
mat_usage.OffsetFromReferenceLine = -mat_usage.OffsetFromReferenceLine
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
if mirror_ref:
|
||||
# Reflect object origin about the mirror reference's YZ plane
|
||||
origin_in_mirror = mirror_ref.matrix_world.inverted() @ obj.matrix_world.translation
|
||||
origin_in_mirror.x *= -1
|
||||
obj.matrix_world.translation = mirror_ref.matrix_world @ origin_in_mirror
|
||||
else:
|
||||
# Fall back: nudge in place to compensate for bounding box shift after inversion
|
||||
mirrored_bb_data = tool.Blender.get_object_bounding_box(obj)
|
||||
x_correction_factor = mirrored_bb_data["min_x"] - bb_data["min_x"]
|
||||
x_correction_vec = (obj.matrix_world @ Vector((x_correction_factor, 0, 0, 0))).xyz
|
||||
obj.location -= x_correction_vec
|
||||
|
||||
if element.is_a("IfcElement") and element.FillsVoids:
|
||||
tool.Model.update_simple_openings(element)
|
||||
|
||||
# For the assign_inverted_type path, mirror opening placements here (after origin reflection
|
||||
# so obj.matrix_world already has the final rotation, needed to compute frame_change).
|
||||
# The LAYER3 / invert_representation path already did this before invert_representation.
|
||||
if is_assign_type_path and opening_placements_before and M_slab_before is not None:
|
||||
R_elem_old = M_slab_before[:3, :3]
|
||||
_, R_quat, _ = obj.matrix_world.decompose()
|
||||
R_elem_new = np.array(R_quat.to_matrix())
|
||||
frame_change = np.linalg.inv(R_elem_new) @ R_elem_old
|
||||
# Sync element IFC placement to the post-reflection Blender position NOW, before
|
||||
# computing opening positions. The geometry engine uses the element's IFC placement
|
||||
# to convert opening world-space positions to element-local positions when applying
|
||||
# boolean voids. If the placement is stale the void lands at the wrong offset.
|
||||
# Calling edit_object_placement here also makes the depsgraph handler skip a
|
||||
# redundant second call (record_object_position marks it up-to-date).
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj, apply_scale=False)
|
||||
M_slab_new = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
self._apply_opening_mirror(element, mirror_axes, M_slab_before, opening_placements_before, frame_change, M_slab_new)
|
||||
for rel in element.HasOpenings:
|
||||
opening = rel.RelatedOpeningElement
|
||||
tool.Geometry.clear_cache(opening)
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
if opening_obj:
|
||||
tool.Geometry.reload_representation(opening_obj)
|
||||
|
||||
# bonsai does not automatically switch to the representation that should be active in the given context
|
||||
# when switching to a type that was previously viewed in another context (e.g. plan view),
|
||||
# the wrong representation will be used.
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=ifcopenshell.util.representation.get_representation(element, active_context),
|
||||
)
|
||||
|
||||
def _apply_opening_mirror(self, element, mirror_axes, M_slab_before, opening_placements_before, frame_change, M_slab_new=None):
|
||||
"""Mirror IfcOpeningElement placements and geometry in element-local space.
|
||||
|
||||
frame_change = inv(R_elem_new) @ R_elem_old accounts for any element rotation change
|
||||
(e.g. 180° Z for Y-mirror via assign_inverted_type). Pass np.eye(3) when the element
|
||||
rotation does not change (LAYER3 / invert_representation path).
|
||||
|
||||
M_slab_new: element IFC world matrix after the mirror. If None, M_slab_before is used
|
||||
(correct for the LAYER3 path where the element placement stays the same).
|
||||
"""
|
||||
M_slab_anchor = M_slab_new if M_slab_new is not None else M_slab_before
|
||||
N_local = np.zeros(3)
|
||||
for i, flip in enumerate(mirror_axes[:3]):
|
||||
if flip > 0.0:
|
||||
N_local[i] = 1.0
|
||||
norm = np.linalg.norm(N_local)
|
||||
if norm > 0:
|
||||
N_local /= norm
|
||||
|
||||
for rel in element.HasOpenings:
|
||||
opening = rel.RelatedOpeningElement
|
||||
if opening.id() not in opening_placements_before:
|
||||
continue
|
||||
M_abs_old = opening_placements_before[opening.id()]
|
||||
M_rel = np.linalg.inv(M_slab_before) @ M_abs_old
|
||||
M_rel_new = M_rel.copy()
|
||||
|
||||
t = M_rel[:3, 3].copy()
|
||||
t_refl = t - 2 * np.dot(t, N_local) * N_local
|
||||
M_rel_new[:3, 3] = frame_change @ t_refl
|
||||
|
||||
# Mirror rotation by conjugation: H@R@H keeps det=+1 and gives the correct mirrored
|
||||
# rotation. Direct Householder on columns yields det=-1; IFC's Y=Z×X normalization
|
||||
# then introduces a spurious 180°Z error. Conjugation avoids this.
|
||||
H = np.eye(3) - 2 * np.outer(N_local, N_local)
|
||||
R_mirrored = H @ M_rel[:3, :3] @ H
|
||||
M_rel_new[:3, :3] = frame_change @ R_mirrored
|
||||
|
||||
M_abs_new = M_slab_anchor @ M_rel_new
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
tool.Ifc.get(), product=opening, matrix=M_abs_new, is_si=False
|
||||
)
|
||||
|
||||
if not opening.Representation:
|
||||
continue
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
mirror_axes_2d = mirror_axes[:2]
|
||||
for rep in opening.Representation.Representations:
|
||||
for item in rep.Items:
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
# H (wall-local Householder) is already computed above for the placement
|
||||
# mirror. Applying it as T_geom to opening-local coords is correct because:
|
||||
# M_rel_new @ T_geom = H @ M_rel (the true geometric mirror)
|
||||
# with M_rel_new = H@R@H (conjugation) → T_geom = H (wall-local numerics
|
||||
# applied to opening-local coords). Conjugating into Position-local gives:
|
||||
# H_pos = placement_mat.T @ H @ placement_mat
|
||||
placement_mat = ifcopenshell.util.placement.get_axis2placement(item.Position)[:3, :3]
|
||||
H_pos = placement_mat.T @ H @ placement_mat
|
||||
|
||||
# Mirror Position.Location (opening-local 3D point) by H
|
||||
pos_coords = item.Position.Location.Coordinates
|
||||
pos3 = np.array([pos_coords[0], pos_coords[1], pos_coords[2] if len(pos_coords) > 2 else 0.0])
|
||||
pos3_new = H @ pos3
|
||||
if len(pos_coords) > 2:
|
||||
item.Position.Location.Coordinates = tuple(float(v) for v in pos3_new)
|
||||
else:
|
||||
item.Position.Location.Coordinates = (float(pos3_new[0]), float(pos3_new[1]))
|
||||
|
||||
# Mirror profile coords in Position-local via H_pos.
|
||||
# H_pos is a reflection (det=-1) so winding must be reversed.
|
||||
profile = item.SweptArea
|
||||
for curve in [getattr(profile, "OuterCurve", None)]:
|
||||
if curve is None:
|
||||
continue
|
||||
coords = builder.get_polyline_coords(curve)
|
||||
coords3 = np.hstack([coords, np.zeros((len(coords), 1))])
|
||||
coords_new = (H_pos @ coords3.T).T[:, :2][::-1]
|
||||
builder.set_polyline_coords(curve, coords_new)
|
||||
for inner in getattr(profile, "InnerCurves", None) or []:
|
||||
coords = builder.get_polyline_coords(inner)
|
||||
coords3 = np.hstack([coords, np.zeros((len(coords), 1))])
|
||||
coords_new = (H_pos @ coords3.T).T[:, :2][::-1]
|
||||
builder.set_polyline_coords(inner, coords_new)
|
||||
|
||||
# Mirror ExtrudedDirection in Position-local via H_pos
|
||||
dir_local = np.array(item.ExtrudedDirection.DirectionRatios)
|
||||
dir_local_new = H_pos @ dir_local
|
||||
item.ExtrudedDirection.DirectionRatios = tuple(float(v) for v in dir_local_new)
|
||||
else:
|
||||
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
||||
|
||||
def invert_general_object(self, element, mirror_axes=(1, 0, 0)):
|
||||
# ShapeBuilder.mirror works in 2D; use only the XY components
|
||||
mirror_axes_2d = mirror_axes[:2]
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
|
||||
def mirror_item(item):
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
mirror_item(item.FirstOperand)
|
||||
try:
|
||||
mirror_item(item.SecondOperand)
|
||||
except Exception:
|
||||
pass
|
||||
elif item.is_a("IfcFacetedBrep") or item.is_a("IfcFacetedBrepWithVoids"):
|
||||
shells = [item.Outer]
|
||||
if item.is_a("IfcFacetedBrepWithVoids"):
|
||||
shells.extend(item.Voids)
|
||||
|
||||
# Mirror all unique vertex coordinates along the flipped axes
|
||||
points_done = set()
|
||||
for shell in shells:
|
||||
for face in shell.CfsFaces:
|
||||
for bound in face.Bounds:
|
||||
if not bound.Bound.is_a("IfcPolyLoop"):
|
||||
continue
|
||||
for pt in bound.Bound.Polygon:
|
||||
if pt.id() in points_done:
|
||||
continue
|
||||
points_done.add(pt.id())
|
||||
coords = list(pt.Coordinates)
|
||||
for i, flip in enumerate(mirror_axes[: len(coords)]):
|
||||
if flip > 0.0:
|
||||
coords[i] = -coords[i]
|
||||
pt.Coordinates = coords
|
||||
|
||||
# Reverse face winding for an odd number of flipped axes (restores outward normals)
|
||||
num_flipped = sum(1 for v in mirror_axes if v > 0.0)
|
||||
if num_flipped % 2 == 1:
|
||||
for shell in shells:
|
||||
for face in shell.CfsFaces:
|
||||
for bound in face.Bounds:
|
||||
if bound.Bound.is_a("IfcPolyLoop"):
|
||||
bound.Bound.Polygon = list(reversed(bound.Bound.Polygon))
|
||||
elif item.is_a("IfcMappedItem"):
|
||||
for sub in item.MappingSource.MappedRepresentation.Items:
|
||||
mirror_item(sub)
|
||||
else:
|
||||
try:
|
||||
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if element.is_a("IfcProduct"):
|
||||
if not element.Representation:
|
||||
return
|
||||
for representation in element.Representation.Representations:
|
||||
for item in representation.Items:
|
||||
mirror_item(item)
|
||||
elif element.is_a("IfcTypeProduct"):
|
||||
for representation_map in (element.RepresentationMaps or []):
|
||||
for item in representation_map.MappedRepresentation.Items:
|
||||
mirror_item(item)
|
||||
|
||||
tool.Geometry.reload_representation(tool.Ifc.get_object(element))
|
||||
|
||||
def invert_door_swing(self, element):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
|
||||
pset_data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
|
||||
|
||||
if "LEFT" in pset_data["door_type"]:
|
||||
pset_data["door_type"] = pset_data["door_type"].replace("LEFT", "RIGHT")
|
||||
elif "RIGHT" in pset_data["door_type"]:
|
||||
pset_data["door_type"] = pset_data["door_type"].replace("RIGHT", "LEFT")
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
pset_data_str = tool.Ifc.get().createIfcText(json.dumps(pset_data, default=list))
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": pset_data_str})
|
||||
|
||||
pset_data.update(pset_data.pop("lining_properties"))
|
||||
pset_data.update(pset_data.pop("panel_properties"))
|
||||
pset_data.update(tool.Model.get_constituents_props_data(element))
|
||||
|
||||
# we need this workaround because set_props_kwargs_from_ifc_data will
|
||||
# "update" the mesh of the active object, which will switch its representation
|
||||
prev_active = bpy.context.view_layer.objects.active
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
|
||||
props = tool.Model.get_door_props(obj)
|
||||
props.set_props_kwargs_from_ifc_data(pset_data)
|
||||
|
||||
bpy.context.view_layer.objects.active = prev_active
|
||||
|
||||
# regenerate door geometry
|
||||
update_door_modifier_representation(obj)
|
||||
|
||||
tool.Model.mark_thumbnail_for_update(element)
|
||||
|
||||
def invert_representation(self, element, mirror_axes=(1, 0, 0)):
|
||||
if ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"):
|
||||
self.invert_door_swing(element)
|
||||
else:
|
||||
self.invert_general_object(element, mirror_axes)
|
||||
|
||||
def assign_inverted_type(self, element, mirror_axes=(1, 0, 0)):
|
||||
type_element = ifcopenshell.util.element.get_type(element)
|
||||
|
||||
inverted_type = tool.Blender.Modifier.has_mirrored_type(type_element)
|
||||
if not inverted_type:
|
||||
old_to_new, _ = tool.Geometry.duplicate_ifc_objects([tool.Ifc.get_object(type_element)])
|
||||
inverted_type = old_to_new[type_element][0]
|
||||
self.invert_representation(inverted_type) # always X-flip for the cached type
|
||||
tool.Blender.Modifier.set_mirrored_type(inverted_type, type_element)
|
||||
tool.Blender.Modifier.set_mirrored_type(type_element, inverted_type)
|
||||
inverted_type.Name = f"{inverted_type.Name}.Mirror"
|
||||
|
||||
bonsai.core.type.assign_type(tool.Ifc, tool.Model, tool.Type, element, inverted_type)
|
||||
|
||||
# The cached type is always X-flipped. Apply rotation compensation for other axes:
|
||||
# flip_Y = Rotate_Z_180 ∘ flip_X
|
||||
# flip_Z = Rotate_Y_180 ∘ flip_X
|
||||
if mirror_axes == (0.0, 1.0, 0.0):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
loc = obj.matrix_world.translation.copy()
|
||||
rot_180_z = Matrix.Rotation(math.pi, 4, "Z")
|
||||
obj.matrix_world = rot_180_z @ obj.matrix_world
|
||||
obj.matrix_world.translation = loc
|
||||
elif mirror_axes == (0.0, 0.0, 1.0):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
loc = obj.matrix_world.translation.copy()
|
||||
rot_180_y = Matrix.Rotation(math.pi, 4, "Y")
|
||||
obj.matrix_world = rot_180_y @ obj.matrix_world
|
||||
obj.matrix_world.translation = loc
|
||||
|
||||
|
||||
class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.mirror_geometry"
|
||||
|
||||
@@ -1093,8 +1093,6 @@ class EditObjectUI:
|
||||
def draw_flip(cls, ui_context, layout) -> None:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else layout
|
||||
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else layout
|
||||
add_layout_hotkey_operator(row, "Mirror Geometry", "C_F", bpy.ops.bim.mirror_geometry.__doc__, ui_context)
|
||||
|
||||
@classmethod
|
||||
def draw_mirror_geometry(cls, ui_context, layout) -> None:
|
||||
@@ -1303,7 +1301,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def hotkey_C_F(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
bpy.ops.bim.mirror_geometry()
|
||||
bpy.ops.bim.mirror_geometry(keep_original=getattr(self, "shift", False))
|
||||
|
||||
def hotkey_S_G(self):
|
||||
obj = bpy.context.active_object
|
||||
|
||||
Reference in New Issue
Block a user