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:
Dion Moult
2023-03-23 21:50:50 +11:00
parent ef2cec7435
commit 0a22714fb8
8 changed files with 144 additions and 10 deletions
@@ -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()
+1
View File
@@ -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="")
+2 -1
View File
@@ -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:
+2 -2
View File
@@ -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
+4 -2
View File
@@ -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):