New feature to resize / squash / stretch objects to their storey height. Useful for remodeling IFCs for construction simulations.

This commit is contained in:
Dion Moult
2021-10-18 14:15:16 +11:00
parent c78e7b3ba8
commit 33f4461a87
12 changed files with 369 additions and 8 deletions
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator
classes = (
operator.ResizeToStorey,
operator.SetOverrideColour,
operator.SetViewportShadowFromSun,
operator.SnapSpacesTogether,
@@ -18,6 +18,8 @@
import bpy
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.misc as core
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector, Matrix, Euler
@@ -109,3 +111,17 @@ class SnapSpacesTogether(bpy.types.Operator):
for v in polygon.vertices:
obj.data.vertices[v].co += offset
return {"FINISHED"}
class ResizeToStorey(bpy.types.Operator, Operator):
bl_idname = "bim.resize_to_storey"
bl_label = "Resize To Storey"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
for obj in context.selected_objects:
core.resize_to_storey(tool.Misc, obj=obj)
@@ -34,7 +34,9 @@ class BIM_PT_misc_utilities(bpy.types.Panel):
row = layout.split(factor=0.2, align=True)
row.prop(props, "override_colour", text="")
row.operator("bim.set_override_colour")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.set_viewport_shadow_from_sun")
row = layout.row(align=True)
row = layout.row()
row.operator("bim.snap_spaces_together")
row = layout.row()
row.operator("bim.resize_to_storey")
+30
View File
@@ -0,0 +1,30 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
def resize_to_storey(misc, obj=None):
storey = misc.get_object_storey(obj)
if not storey:
return
height = misc.get_storey_height_in_si(storey)
if not height:
return
misc.set_object_origin_to_bottom(obj)
misc.move_object_to_elevation(obj, misc.get_storey_elevation_in_si(storey))
misc.scale_object_to_height(obj, height)
misc.mark_object_as_edited(obj)
+11
View File
@@ -77,6 +77,17 @@ class Material:
def unlink(cls, obj): pass
@interface
class Misc:
def get_object_storey(cls, obj): pass
def get_storey_elevation_in_si(cls, storey): pass
def get_storey_height_in_si(cls, storey): pass
def mark_object_as_edited(cls, obj): pass
def move_object_to_elevation(cls, obj, elevation): pass
def scale_object_to_height(cls, obj, height): pass
def set_object_origin_to_bottom(cls, obj): pass
@interface
class Owner:
def add_address_attribute(cls, name): pass
@@ -23,6 +23,7 @@ from blenderbim.tool.container import Container
from blenderbim.tool.context import Context
from blenderbim.tool.ifc import Ifc
from blenderbim.tool.material import Material
from blenderbim.tool.misc import Misc
from blenderbim.tool.owner import Owner
from blenderbim.tool.style import Style
from blenderbim.tool.surveyor import Surveyor
+1 -3
View File
@@ -60,9 +60,7 @@ class Container(blenderbim.core.tool.Container):
for rel in parent.IsDecomposedBy or []:
related_objects = []
for element in rel.RelatedObjects:
related_objects.append(
(element, ifcopenshell.util.placement.get_storey_elevation(element))
)
related_objects.append((element, ifcopenshell.util.placement.get_storey_elevation(element)))
related_objects = sorted(related_objects, key=lambda e: e[1])
for element in related_objects:
element = element[0]
+91
View File
@@ -0,0 +1,91 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
class Misc(blenderbim.core.tool.Misc):
@classmethod
def get_object_storey(cls, obj):
storey = ifcopenshell.util.element.get_container(tool.Ifc.get_entity(obj))
if storey and storey.is_a("IfcBuildingStorey"):
return storey
@classmethod
def get_storey_elevation_in_si(cls, storey):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
elevation = ifcopenshell.util.placement.get_storey_elevation(storey)
return elevation * unit_scale
@classmethod
def get_storey_height_in_si(cls, storey):
building = ifcopenshell.util.element.get_aggregate(storey)
related_objects = []
for rel in building.IsDecomposedBy:
if rel.is_a("IfcRelAggregates"):
for element in rel.RelatedObjects:
if element.is_a("IfcBuildingStorey"):
related_objects.append((element, ifcopenshell.util.placement.get_storey_elevation(element)))
related_objects = sorted(related_objects, key=lambda e: e[1])
storey_elevation = None
next_storey_elevation = None
for related_object in related_objects:
if related_object[0] == storey:
storey_elevation = related_object[1]
elif storey_elevation is not None:
next_storey_elevation = related_object[1]
break
if next_storey_elevation:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
return (next_storey_elevation - storey_elevation) * unit_scale
@classmethod
def set_object_origin_to_bottom(cls, obj):
absolute_bound_box = [obj.matrix_world @ Vector(c) for c in obj.bound_box]
min_z = min([c[2] for c in absolute_bound_box])
new_origin = obj.matrix_world.translation.copy()
new_origin[2] = min_z
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
@classmethod
def move_object_to_elevation(cls, obj, elevation):
obj.matrix_world.translation[2] = elevation
@classmethod
def scale_object_to_height(cls, obj, height):
absolute_bound_box = [obj.matrix_world @ Vector(c) for c in obj.bound_box]
max_z = max([c[2] for c in absolute_bound_box])
min_z = min([c[2] for c in absolute_bound_box])
current_absolute_height = max_z - min_z
scale_factor = height / current_absolute_height
obj.matrix_world @= Matrix.Scale(scale_factor, 4, obj.matrix_world.to_quaternion() @ Vector((0, 0, 1)))
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
@classmethod
def mark_object_as_edited(cls, obj):
IfcStore.edited_objs.add(obj)
+15 -3
View File
@@ -2,24 +2,36 @@
Feature: Misc
Scenario: Set override colour
Given an empty IFC project
Given an empty Blender session
And I add a cube
And the object "Cube" is selected
When I press "bim.set_override_colour"
Then nothing happens
Scenario: Set viewport shadow from sun
Given an empty IFC project
Given an empty Blender session
And I add a sun
And the object "Sun" is selected
When I press "bim.set_viewport_shadow_from_sun"
Then nothing happens
Scenario: Snap spaces together
Given an empty IFC project
Given an empty Blender session
And I add a cube
And I add a cube
And the object "Cube" is selected
And additionally the object "Cube.001" is selected
When I press "bim.snap_spaces_together"
Then nothing happens
Scenario: Resize to storey
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
And I press "bim.assign_container(structure={storey})"
When I press "bim.resize_to_storey"
Then nothing happens
+7
View File
@@ -56,6 +56,13 @@ def material():
prophet.verify()
@pytest.fixture
def misc():
prophet = Prophecy(blenderbim.core.tool.Misc)
yield prophet
prophet.verify()
@pytest.fixture
def owner():
prophet = Prophecy(blenderbim.core.tool.Owner)
+41
View File
@@ -0,0 +1,41 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blenderbim.core.misc as subject
from test.core.bootstrap import misc
class TestResizeToStorey:
def test_run(self, misc):
misc.get_object_storey("obj").should_be_called().will_return("storey")
misc.get_storey_elevation_in_si("storey").should_be_called().will_return("elevation")
misc.get_storey_height_in_si("storey").should_be_called().will_return("height")
misc.set_object_origin_to_bottom("obj").should_be_called()
misc.move_object_to_elevation("obj", "elevation").should_be_called()
misc.scale_object_to_height("obj", "height").should_be_called()
misc.mark_object_as_edited("obj").should_be_called()
subject.resize_to_storey(misc, obj="obj")
def test_doing_nothing_when_the_object_has_no_storey(self, misc):
misc.get_object_storey("obj").should_be_called().will_return(None)
subject.resize_to_storey(misc, obj="obj")
def test_doing_nothing_when_the_storey_has_no_height(self, misc):
misc.get_object_storey("obj").should_be_called().will_return("storey")
misc.get_storey_height_in_si("storey").should_be_called().will_return(None)
subject.resize_to_storey(misc, obj="obj")
+151
View File
@@ -0,0 +1,151 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import math
import numpy
import ifcopenshell
import test.bim.bootstrap
import blenderbim.core.tool
import blenderbim.tool as tool
from blenderbim.tool.misc import Misc as subject
from blenderbim.bim.ifc import IfcStore
class TestImplementsTool(test.bim.bootstrap.NewFile):
def test_run(self):
assert isinstance(subject(), blenderbim.core.tool.Misc)
class TestGetObjectStorey(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
wall = ifc.createIfcWall()
tool.Ifc.link(wall, obj)
storey = ifc.createIfcBuildingStorey()
ifcopenshell.api.run("spatial.assign_container", ifc, product=wall, relating_structure=storey)
assert subject.get_object_storey(obj) == storey
def test_only_returning_a_building_storey(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
wall = ifc.createIfcWall()
tool.Ifc.link(wall, obj)
building = ifc.createIfcBuilding()
ifcopenshell.api.run("spatial.assign_container", ifc, product=wall, relating_structure=building)
assert subject.get_object_storey(obj) is None
def test_returning_nothing_if_uncontained(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
wall = ifc.createIfcWall()
tool.Ifc.link(wall, obj)
assert subject.get_object_storey(obj) is None
class TestGetStoreyElevation(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
ifc.createIfcProject()
unit = ifcopenshell.api.run("unit.add_si_unit", ifc, prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit])
storey = ifc.createIfcBuildingStorey()
storey.Elevation = 3000
tool.Ifc.set(ifc)
assert subject.get_storey_elevation_in_si(storey) == 3.0
class TestGetStoreyHeight(test.bim.bootstrap.NewFile):
def test_run(self):
ifc = ifcopenshell.file()
ifc.createIfcProject()
unit = ifcopenshell.api.run("unit.add_si_unit", ifc, prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit])
tool.Ifc.set(ifc)
building = ifc.createIfcBuilding()
storey = ifc.createIfcBuildingStorey()
storey.Elevation = 3000
storey2 = ifc.createIfcBuildingStorey()
storey2.Elevation = 5000
ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey, relating_object=building)
ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey2, relating_object=building)
assert subject.get_storey_height_in_si(storey) == 2.0
def test_only_considering_storeys_in_the_same_building(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
building = ifc.createIfcBuilding()
storey = ifc.createIfcBuildingStorey()
storey.Elevation = 3000
storey2 = ifc.createIfcBuildingStorey()
storey2.Elevation = 5000
ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey, relating_object=building)
assert subject.get_storey_height_in_si(storey) is None
def test_returning_none_if_the_storey_height_is_undefined(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
building = ifc.createIfcBuilding()
storey = ifc.createIfcBuildingStorey()
ifcopenshell.api.run("aggregate.assign_object", ifc, product=storey, relating_object=building)
assert subject.get_storey_height_in_si(storey) is None
class TestSetObjectOriginToBottom(test.bim.bootstrap.NewFile):
def test_run(self):
bpy.ops.mesh.primitive_cube_add()
obj = bpy.data.objects.get("Cube")
subject.set_object_origin_to_bottom(obj)
assert list(obj.matrix_world.translation) == [0.0, 0.0, -1.0]
def test_moving_the_origin_of_a_rotated_object(self):
bpy.ops.mesh.primitive_cube_add()
obj = bpy.data.objects.get("Cube")
obj.rotation_euler[1] = math.pi / 2
bpy.context.view_layer.update()
subject.set_object_origin_to_bottom(obj)
assert list(obj.matrix_world.translation) == [0.0, 0.0, -1.0]
class TestMoveObjectToElevation(test.bim.bootstrap.NewFile):
def test_run(self):
bpy.ops.mesh.primitive_cube_add()
obj = bpy.data.objects.get("Cube")
subject.move_object_to_elevation(obj, 3)
assert list(obj.matrix_world.translation) == [0.0, 0.0, 3.0]
class TestScaleObjectToHeight(test.bim.bootstrap.NewFile):
def test_run(self):
bpy.ops.mesh.primitive_cube_add()
obj = bpy.data.objects.get("Cube")
subject.set_object_origin_to_bottom(obj)
subject.scale_object_to_height(obj, 3)
assert obj.dimensions[2] == 3.0
assert list(obj.scale) == [1.0, 1.0, 1.0]
class TestMarkObjectAsEdited(test.bim.bootstrap.NewFile):
def test_run(self):
obj = bpy.data.objects.new("Cube", None)
subject.mark_object_as_edited(obj)
assert obj in IfcStore.edited_objs