mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
New feature to resize / squash / stretch objects to their storey height. Useful for remodeling IFCs for construction simulations.
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user