mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-12 06:32:09 +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.OverrideDelete,
|
||||||
operator.OverrideDuplicateMove,
|
operator.OverrideDuplicateMove,
|
||||||
operator.OverrideDuplicateMoveLinked,
|
operator.OverrideDuplicateMoveLinked,
|
||||||
|
operator.OverrideModeSet,
|
||||||
operator.OverrideOutlinerDelete,
|
operator.OverrideOutlinerDelete,
|
||||||
operator.OverridePasteBuffer,
|
operator.OverridePasteBuffer,
|
||||||
operator.RemoveConnection,
|
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", "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_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_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", "X", "PRESS")
|
||||||
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
|
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
|
||||||
kmi.properties.confirm = False
|
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")
|
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_paste_buffer", "V", "PRESS", ctrl=True)
|
||||||
kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
|
kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import bmesh
|
import bmesh
|
||||||
|
import struct
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
@@ -620,3 +622,126 @@ class OverridePasteBuffer(bpy.types.Operator):
|
|||||||
for obj in context.selected_objects:
|
for obj in context.selected_objects:
|
||||||
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
|
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
|
||||||
return {"FINISHED"}
|
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_definition: StringProperty(name="IFC Definition")
|
||||||
ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter)
|
ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter)
|
||||||
material_checksum: StringProperty(name="Material Checksum", default="[]")
|
material_checksum: StringProperty(name="Material Checksum", default="[]")
|
||||||
|
mesh_checksum: StringProperty(name="Mesh Checksum", default="")
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ def switch_representation(
|
|||||||
should_reload=True,
|
should_reload=True,
|
||||||
is_global=True,
|
is_global=True,
|
||||||
should_sync_changes_first=False,
|
should_sync_changes_first=False,
|
||||||
|
apply_openings=True,
|
||||||
):
|
):
|
||||||
"""Function can switch to representation that wasn't yet assigned to that object. See #2766."""
|
"""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):
|
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)
|
existing_data = geometry.get_representation_data(representation)
|
||||||
|
|
||||||
if should_reload or not existing_data:
|
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.rename_object(data, geometry.get_representation_name(representation))
|
||||||
geometry.link(representation, data)
|
geometry.link(representation, data)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
|||||||
return data.users != 0
|
return data.users != 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_representation(cls, obj, representation):
|
def import_representation(cls, obj, representation, apply_openings=True):
|
||||||
logger = logging.getLogger("ImportIFC")
|
logger = logging.getLogger("ImportIFC")
|
||||||
ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
|
ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
@@ -178,7 +178,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
|||||||
|
|
||||||
context = representation.ContextOfItems
|
context = representation.ContextOfItems
|
||||||
if context.ContextIdentifier == "Body" and context.TargetView == "MODEL_VIEW":
|
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)
|
shape = ifcopenshell.geom.create_shape(settings, representation)
|
||||||
else:
|
else:
|
||||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Scenario: Add representation
|
|||||||
And the object "IfcWall/Cube" is selected
|
And the object "IfcWall/Cube" is selected
|
||||||
Then the object "IfcWall/Cube" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
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()"
|
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"
|
And I press "bim.add_representation"
|
||||||
Then the object "IfcWall/Cube" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
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"
|
And the object "IfcWall/Wall.001" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||||
When the object "IfcWall/Wall" is selected
|
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 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 I press "bim.add_representation"
|
||||||
Then the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
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"
|
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 set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||||
And I press "bim.assign_class"
|
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 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"
|
And I press "bim.add_representation"
|
||||||
When the object "IfcWall/Cube" is scaled to "2"
|
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()"
|
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" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
|
||||||
And the object "IfcWall/Wall" is selected
|
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 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 I press "bim.add_representation"
|
||||||
And the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
And the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW"
|
||||||
When I press "bim.add_constr_type_instance"
|
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 undo
|
||||||
And I press "mesh.add_door()"
|
And I press "mesh.add_door()"
|
||||||
Then nothing happens
|
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.is_edited("obj").should_be_called().will_return(False)
|
||||||
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
|
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
|
||||||
geometry.get_representation_data("representation").should_be_called().will_return(None)
|
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.get_representation_name("representation").should_be_called().will_return("name")
|
||||||
geometry.rename_object("new_data", "name").should_be_called()
|
geometry.rename_object("new_data", "name").should_be_called()
|
||||||
geometry.link("representation", "new_data").should_be_called()
|
geometry.link("representation", "new_data").should_be_called()
|
||||||
@@ -210,13 +210,14 @@ class TestSwitchRepresentation:
|
|||||||
should_reload=True,
|
should_reload=True,
|
||||||
is_global=True,
|
is_global=True,
|
||||||
should_sync_changes_first=True,
|
should_sync_changes_first=True,
|
||||||
|
apply_openings=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_switching_to_a_reloaded_representation_and_deleting_the_existing_data(self, ifc, geometry):
|
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.is_edited("obj").should_be_called().will_return(False)
|
||||||
geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation")
|
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.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.get_representation_name("representation").should_be_called().will_return("name")
|
||||||
geometry.rename_object("new_data", "name").should_be_called()
|
geometry.rename_object("new_data", "name").should_be_called()
|
||||||
geometry.link("representation", "new_data").should_be_called()
|
geometry.link("representation", "new_data").should_be_called()
|
||||||
@@ -233,6 +234,7 @@ class TestSwitchRepresentation:
|
|||||||
should_reload=True,
|
should_reload=True,
|
||||||
is_global=True,
|
is_global=True,
|
||||||
should_sync_changes_first=True,
|
should_sync_changes_first=True,
|
||||||
|
apply_openings=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_switching_to_an_existing_representation(self, ifc, geometry):
|
def test_switching_to_an_existing_representation(self, ifc, geometry):
|
||||||
|
|||||||
Reference in New Issue
Block a user