mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
See #2887. Experimental edit mode toggle override to ensure mesh changes stay in sync and so that you can edit meshes without openings applied.
This commit is contained in:
@@ -27,6 +27,7 @@ classes = (
|
||||
operator.OverrideDelete,
|
||||
operator.OverrideDuplicateMove,
|
||||
operator.OverrideDuplicateMoveLinked,
|
||||
operator.OverrideModeSet,
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverridePasteBuffer,
|
||||
operator.RemoveConnection,
|
||||
@@ -59,10 +60,14 @@ def register():
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True)
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True)
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
kmi = km.keymap_items.new("bim.override_mode_set", "TAB", "PRESS")
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
|
||||
kmi.properties.confirm = False
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set", "TAB", "PRESS")
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import struct
|
||||
import hashlib
|
||||
import logging
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
@@ -620,3 +622,126 @@ class OverridePasteBuffer(bpy.types.Operator):
|
||||
for obj in context.selected_objects:
|
||||
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OverrideModeSet(bpy.types.Operator):
|
||||
bl_idname = "bim.override_mode_set"
|
||||
bl_label = "IFC Mode Set"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
should_save: bpy.props.BoolProperty(name="Should Save", default=True)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object
|
||||
|
||||
def execute(self, context):
|
||||
objs = context.selected_objects or [context.active_object]
|
||||
context.active_object.select_set(True)
|
||||
edited_objs = []
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
|
||||
if self.active_mode == "EDIT":
|
||||
# We are switching from EDIT to OBJECT mode.
|
||||
if obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
if representation.RepresentationType in ("Tessellation", "Brep"):
|
||||
edited_objs.append(obj)
|
||||
else:
|
||||
# We are switching from OBJECT to EDIT mode.
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
if usage_type:
|
||||
# Parametric objects shall not be edited as meshes as they
|
||||
# can be modified to be incompatible with the parametric
|
||||
# constraints.
|
||||
obj.select_set(False)
|
||||
continue
|
||||
|
||||
if obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
if representation.RepresentationType in ("Tessellation", "Brep"):
|
||||
if element.HasOpenings:
|
||||
# Mesh elements with openings must disable openings
|
||||
# so that you can edit the original topology.
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
obj.data.BIMMeshProperties.mesh_checksum = self.get_mesh_checksum(obj.data)
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
|
||||
for obj in edited_objs:
|
||||
if self.should_save and obj.data.BIMMeshProperties.mesh_checksum != self.get_mesh_checksum(obj.data):
|
||||
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
|
||||
if element.HasOpenings:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
# Mesh elements with openings must disable openings
|
||||
# so that you can edit the original topology.
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
else:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
if self.active_mode == "EDIT":
|
||||
row.prop(self, "should_save")
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not tool.Ifc.get():
|
||||
return bpy.ops.object.mode_set(mode="EDIT", toggle=True)
|
||||
objs = context.selected_objects or [context.active_object]
|
||||
obj = objs[0]
|
||||
self.active_mode = obj.mode
|
||||
if self.active_mode == "EDIT":
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
return self.execute(context)
|
||||
|
||||
def get_mesh_checksum(self, mesh):
|
||||
# Get mesh data
|
||||
vertices = mesh.vertices[:]
|
||||
edges = mesh.edges[:]
|
||||
faces = mesh.polygons[:]
|
||||
|
||||
# Convert mesh data to bytes
|
||||
data_bytes = b''
|
||||
for v in vertices:
|
||||
data_bytes += struct.pack('3f', *v.co)
|
||||
for e in edges:
|
||||
data_bytes += struct.pack('2i', *e.vertices)
|
||||
for f in faces:
|
||||
data_bytes += struct.pack('%di' % len(f.vertices), *f.vertices)
|
||||
|
||||
# Generate hash of mesh data
|
||||
hasher = hashlib.sha1()
|
||||
hasher.update(data_bytes)
|
||||
return hasher.hexdigest()
|
||||
|
||||
@@ -426,3 +426,4 @@ class BIMMeshProperties(PropertyGroup):
|
||||
ifc_definition: StringProperty(name="IFC Definition")
|
||||
ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter)
|
||||
material_checksum: StringProperty(name="Material Checksum", default="[]")
|
||||
mesh_checksum: StringProperty(name="Mesh Checksum", default="")
|
||||
|
||||
@@ -86,6 +86,7 @@ def switch_representation(
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
):
|
||||
"""Function can switch to representation that wasn't yet assigned to that object. See #2766."""
|
||||
if should_sync_changes_first and geometry.is_edited(obj) and not geometry.is_box_representation(representation):
|
||||
@@ -98,7 +99,7 @@ def switch_representation(
|
||||
existing_data = geometry.get_representation_data(representation)
|
||||
|
||||
if should_reload or not existing_data:
|
||||
data = geometry.import_representation(obj, representation)
|
||||
data = geometry.import_representation(obj, representation, apply_openings=apply_openings)
|
||||
geometry.rename_object(data, geometry.get_representation_name(representation))
|
||||
geometry.link(representation, data)
|
||||
else:
|
||||
|
||||
@@ -169,7 +169,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
return data.users != 0
|
||||
|
||||
@classmethod
|
||||
def import_representation(cls, obj, representation):
|
||||
def import_representation(cls, obj, representation, apply_openings=True):
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -178,7 +178,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
|
||||
context = representation.ContextOfItems
|
||||
if context.ContextIdentifier == "Body" and context.TargetView == "MODEL_VIEW":
|
||||
if element.is_a("IfcTypeProduct"):
|
||||
if element.is_a("IfcTypeProduct") or not apply_openings:
|
||||
shape = ifcopenshell.geom.create_shape(settings, representation)
|
||||
else:
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
|
||||
@@ -20,7 +20,7 @@ Scenario: Add representation
|
||||
And the object "IfcWall/Cube" is selected
|
||||
Then the object "IfcWall/Cube" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
When the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()"
|
||||
And I set "scene.BIMRootProperties.contexts" to "{context}"
|
||||
And I set "active_object.BIMGeometryProperties.contexts" to "{context}"
|
||||
And I press "bim.add_representation"
|
||||
Then the object "IfcWall/Cube" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
||||
|
||||
@@ -37,7 +37,7 @@ Scenario: Add representation - add a new representation to a typed instance
|
||||
And the object "IfcWall/Wall.001" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
When the object "IfcWall/Wall" is selected
|
||||
And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()"
|
||||
And I set "scene.BIMRootProperties.contexts" to "{context}"
|
||||
And I set "active_object.BIMGeometryProperties.contexts" to "{context}"
|
||||
And I press "bim.add_representation"
|
||||
Then the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
||||
And the object "IfcWall/Wall.001" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
||||
@@ -80,7 +80,7 @@ Scenario: Switch representation - current edited representation is updated prior
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier=='Annotation'][0].id()"
|
||||
And I set "scene.BIMRootProperties.contexts" to "{context}"
|
||||
And I set "active_object.BIMGeometryProperties.contexts" to "{context}"
|
||||
And I press "bim.add_representation"
|
||||
When the object "IfcWall/Cube" is scaled to "2"
|
||||
And the variable "representation" is "[r for r in {ifc}.by_type('IfcShapeRepresentation') if r.RepresentationType=='Tessellation'][0].id()"
|
||||
|
||||
@@ -41,7 +41,7 @@ Scenario: Add type instance - add a mesh where existing instances have changed c
|
||||
And the object "IfcWall/Wall" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||
And the object "IfcWall/Wall" is selected
|
||||
And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()"
|
||||
And I set "scene.BIMRootProperties.contexts" to "{context}"
|
||||
And I set "active_object.BIMGeometryProperties.contexts" to "{context}"
|
||||
And I press "bim.add_representation"
|
||||
And the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
||||
When I press "bim.add_constr_type_instance"
|
||||
@@ -524,4 +524,4 @@ Scenario: Create a door, undo and create a new door
|
||||
And I undo
|
||||
And I press "mesh.add_door()"
|
||||
Then nothing happens
|
||||
And the object "IfcDoor/IfcDoor" exists
|
||||
And the object "IfcDoor/IfcDoor" exists
|
||||
|
||||
@@ -194,7 +194,7 @@ class TestSwitchRepresentation:
|
||||
geometry.is_edited("obj").should_be_called().will_return(False)
|
||||
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
|
||||
geometry.get_representation_data("representation").should_be_called().will_return(None)
|
||||
geometry.import_representation("obj", "representation").should_be_called().will_return("new_data")
|
||||
geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return("new_data")
|
||||
geometry.get_representation_name("representation").should_be_called().will_return("name")
|
||||
geometry.rename_object("new_data", "name").should_be_called()
|
||||
geometry.link("representation", "new_data").should_be_called()
|
||||
@@ -210,13 +210,14 @@ class TestSwitchRepresentation:
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
def test_switching_to_a_reloaded_representation_and_deleting_the_existing_data(self, ifc, geometry):
|
||||
geometry.is_edited("obj").should_be_called().will_return(False)
|
||||
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
|
||||
geometry.get_representation_data("representation").should_be_called().will_return("existing_data")
|
||||
geometry.import_representation("obj", "representation").should_be_called().will_return("new_data")
|
||||
geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return("new_data")
|
||||
geometry.get_representation_name("representation").should_be_called().will_return("name")
|
||||
geometry.rename_object("new_data", "name").should_be_called()
|
||||
geometry.link("representation", "new_data").should_be_called()
|
||||
@@ -233,6 +234,7 @@ class TestSwitchRepresentation:
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
def test_switching_to_an_existing_representation(self, ifc, geometry):
|
||||
|
||||
Reference in New Issue
Block a user