Skip to content

Commit 67016d6

Browse files
committed
See #2999. Reimplement basic creation of structural items.
1 parent 439f9c1 commit 67016d6

7 files changed

Lines changed: 194 additions & 23 deletions

File tree

src/bonsai/bonsai/bim/module/root/data.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -118,29 +118,31 @@ def ifc_classes_suggestions(cls) -> dict[str, list[dict[str, Union[str, None]]]]
118118
def representation_template(cls):
119119
rprops = tool.Root.get_root_props()
120120
ifc_class = rprops.ifc_class
121+
if ifc_class.startswith("IfcStructuralPoint"):
122+
return [("VERTEX", "Vertex", "A single 3D point")]
123+
elif ifc_class.startswith("IfcStructuralCurve"):
124+
return [("EDGE", "Edge", "A straight edge between two points")]
125+
elif ifc_class.startswith("IfcStructuralSurface"):
126+
return [("FACE", "Face", "A planar face surface")]
121127
templates = [
122128
("EMPTY", "No Geometry", "Start with an empty object"),
123129
None,
130+
(
131+
"OBJ",
132+
"Tessellation From Object",
133+
"Use an object as a template to create a new tessellation",
134+
),
135+
(
136+
"MESH",
137+
"Custom Tessellation",
138+
"Create a basic tessellated or faceted cube",
139+
),
140+
(
141+
"EXTRUSION",
142+
"Custom Extruded Solid",
143+
"An extrusion from an arbitrary profile",
144+
),
124145
]
125-
templates.extend(
126-
[
127-
(
128-
"OBJ",
129-
"Tessellation From Object",
130-
"Use an object as a template to create a new tessellation",
131-
),
132-
(
133-
"MESH",
134-
"Custom Tessellation",
135-
"Create a basic tessellated or faceted cube",
136-
),
137-
(
138-
"EXTRUSION",
139-
"Custom Extruded Solid",
140-
"An extrusion from an arbitrary profile",
141-
),
142-
]
143-
)
144146
if ifc_class.endswith("Type") or ifc_class.endswith("Style"):
145147
templates.extend(
146148
[

src/bonsai/bonsai/bim/module/root/operator.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,15 @@ def _invoke(self, context, event):
467467
props.representation_template = "EXTRUSION"
468468
props.representation_obj = None
469469
elif (obj := tool.Blender.get_active_object(is_selected=True)) and obj.type == "MESH":
470-
props.representation_template = "OBJ"
471-
props.representation_obj = obj
470+
if (
471+
props.ifc_class.startswith("IfcStructuralPoint")
472+
or props.ifc_class.startswith("IfcStructuralCurve")
473+
or props.ifc_class.startswith("IfcStructuralSurface")
474+
):
475+
pass # Implement auto association?
476+
else:
477+
props.representation_template = "OBJ"
478+
props.representation_obj = obj
472479
# For convenience, preselect IFC class
473480
if self.ifc_product:
474481
props.ifc_product = self.ifc_product
@@ -677,6 +684,49 @@ def _execute(self, context):
677684
elif representation_template == "ROOF":
678685
with context.temp_override(active_object=obj, selected_objects=[]):
679686
bpy.ops.bim.add_roof()
687+
elif representation_template == "VERTEX":
688+
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
689+
representation = builder.get_representation(ifc_context, [builder.vertex()])
690+
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
691+
bonsai.core.geometry.switch_representation(
692+
tool.Ifc,
693+
tool.Geometry,
694+
obj=obj,
695+
representation=representation,
696+
should_reload=True,
697+
is_global=True,
698+
should_sync_changes_first=False,
699+
)
700+
elif representation_template == "EDGE":
701+
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
702+
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
703+
end = Vector((1, 0, 0)) / unit_scale
704+
representation = builder.get_representation(ifc_context, [builder.edge(end=end)])
705+
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
706+
bonsai.core.geometry.switch_representation(
707+
tool.Ifc,
708+
tool.Geometry,
709+
obj=obj,
710+
representation=representation,
711+
should_reload=True,
712+
is_global=True,
713+
should_sync_changes_first=False,
714+
)
715+
elif representation_template == "FACE":
716+
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
717+
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
718+
points = [Vector(p) / unit_scale for p in ((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0))]
719+
representation = builder.get_representation(ifc_context, [builder.face(points)])
720+
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
721+
bonsai.core.geometry.switch_representation(
722+
tool.Ifc,
723+
tool.Geometry,
724+
obj=obj,
725+
representation=representation,
726+
should_reload=True,
727+
is_global=True,
728+
should_sync_changes_first=False,
729+
)
680730

681731
bpy.context.view_layer.update() # Ensures obj.matrix_world is correct
682732

src/bonsai/test/bim/feature/structural.feature

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
@structural
22
Feature: Structural
33

4+
Scenario: Add element - a structural point connection
5+
Given an empty IFC project
6+
And I trigger "Add Element"
7+
And I set the "Name" property to "Foo"
8+
And I set the "Definition" property to "IfcStructuralItem"
9+
And I set the "Class" property to "IfcStructuralPointConnection"
10+
And I set the "Representation" property to "Vertex"
11+
When I click "OK"
12+
And I make the collection "IfcStructuralItem" visible
13+
And I select the object "IfcStructuralPointConnection/Foo"
14+
And I toggle edit mode
15+
Then the object "Item/IfcVertexPoint/69" exists
16+
17+
Scenario: Add element - a structural curve member
18+
Given an empty IFC project
19+
And I trigger "Add Element"
20+
And I set the "Name" property to "Foo"
21+
And I set the "Definition" property to "IfcStructuralItem"
22+
And I set the "Class" property to "IfcStructuralCurveMember"
23+
And I set the "Representation" property to "Edge"
24+
When I click "OK"
25+
And I make the collection "IfcStructuralItem" visible
26+
And I select the object "IfcStructuralCurveMember/Foo"
27+
And I toggle edit mode
28+
Then the object "Item/IfcEdge/72" exists
29+
30+
Scenario: Add element - a structural surface member
31+
Given an empty IFC project
32+
And I trigger "Add Element"
33+
And I set the "Name" property to "Foo"
34+
And I set the "Definition" property to "IfcStructuralItem"
35+
And I set the "Class" property to "IfcStructuralSurfaceMember"
36+
And I set the "Representation" property to "Face"
37+
When I click "OK"
38+
And I make the collection "IfcStructuralItem" visible
39+
And I select the object "IfcStructuralSurfaceMember/Foo"
40+
And I toggle edit mode
41+
Then the object "Item/IfcFace/74" exists
42+
443
Scenario: Load structural analysis models
544
Given an empty IFC project
645
When I press "bim.load_structural_analysis_models"

src/bonsai/test/bim/test_feature.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,12 @@ def i_add_a_new_collection_item(collection):
658658
assert False, "Collection does not exist"
659659

660660

661+
@given(parsers.parse('I make the collection "{name}" visible'))
662+
@when(parsers.parse('I make the collection "{name}" visible'))
663+
def i_make_the_collection_name_visible(name):
664+
tool.Blender.get_layer_collection(bpy.data.collections.get(name)).hide_viewport = False
665+
666+
661667
@given(parsers.parse('the material "{name}" colour is set to "{colour}"'))
662668
@when(parsers.parse('the material "{name}" colour is set to "{colour}"'))
663669
def the_material_name_colour_is_set_to_colour(name, colour):

src/ifcopenshell-python/ifcopenshell/util/representation.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,16 @@ def guess_type(items: Sequence[ifcopenshell.entity_instance]) -> Union[str, None
286286
return "SectionedSpine"
287287
elif all([True if i.is_a("IfcLightSource") else False for i in items]):
288288
return "LightSource"
289+
elif all([True if i.is_a("IfcVertex") else False for i in items]):
290+
return "Vertex"
291+
elif all([True if i.is_a("IfcEdge") else False for i in items]):
292+
return "Edge"
293+
elif all([True if i.is_a("IfcPath") else False for i in items]):
294+
return "Path"
295+
elif all([True if i.is_a("IfcFace") else False for i in items]):
296+
return "Face"
297+
elif all([True if i.is_a("IfcOpenShell") else False for i in items]):
298+
return "Shell"
289299

290300

291301
def resolve_representation(representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:

src/ifcopenshell-python/ifcopenshell/util/shape_builder.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,41 @@ def create_axis2_placement_2d(
785785
RefDirection=ref_direction,
786786
)
787787

788+
def vertex(self, position: VectorType = (0.0, 0.0, 0.0)) -> ifcopenshell.entity_instance:
789+
"""Create a topological vertex
790+
791+
Commonly used in structural point elements.
792+
793+
:param position: The 3D coordinate of the vertex
794+
:return: IfcVertexPoint
795+
"""
796+
return self.file.create_entity(
797+
"IfcVertexPoint", self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(position))
798+
)
799+
800+
def edge(
801+
self, start: VectorType = (0.0, 0.0, 0.0), end: VectorType = (1.0, 0.0, 0.0)
802+
) -> ifcopenshell.entity_instance:
803+
"""Create a topological edge
804+
805+
:param start: The start coordinates of the vertex.
806+
:param end: The end coordinates of the vertex.
807+
:return: IfcEdge
808+
"""
809+
return self.file.create_entity("IfcEdge", self.vertex(start), self.vertex(end))
810+
811+
def face(self, points: SequenceOfVectors) -> ifcopenshell.entity_instance:
812+
"""Create a single topological face
813+
814+
There are many types of faces, but for now we only support planar
815+
polyloop defined faces with an outer boundary.
816+
817+
:param points: ordered list of 3d coordinates representing the outer boundary
818+
:return: IfcFace
819+
"""
820+
verts = [self.file.createIfcCartesianPoint(p) for p in ifc_safe_vector_type(points)]
821+
return self.file.createIfcFace([self.file.createIfcFaceOuterBound(self.file.createIfcPolyLoop(verts), True)])
822+
788823
def mirror(
789824
self,
790825
curve_or_item: Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]],
@@ -1025,13 +1060,17 @@ def get_representation(
10251060
if not representation_type:
10261061
representation_type = ifcopenshell.util.representation.guess_type(items)
10271062

1028-
representation = self.file.createIfcShapeRepresentation(
1063+
return self.file.create_entity(
1064+
(
1065+
"IfcTopologyRepresentation"
1066+
if representation_type in ("Vertex", "Edge", "Path", "Face", "Shell")
1067+
else "IfcShapeRepresentation"
1068+
),
10291069
ContextOfItems=context,
10301070
RepresentationIdentifier=context.ContextIdentifier,
10311071
RepresentationType=representation_type,
10321072
Items=items,
10331073
)
1034-
return representation
10351074

10361075
def deep_copy(self, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
10371076
return ifcopenshell.util.element.copy_deep(self.file, element)

src/ifcopenshell-python/test/util/test_shape_builder.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,31 @@ def test_mirror(self):
203203
assert np.allclose(rectangle.Points.CoordList, ((0.0, 0.0), (-100.0, 0.0), (-100.0, 100.0), (0.0, 100.0)))
204204

205205

206+
class TestVertex(test.bootstrap.IFC4):
207+
def test_run(self):
208+
builder = ShapeBuilder(self.file)
209+
vertex = builder.vertex((1, 2, 3))
210+
assert np.allclose(vertex.VertexGeometry.Coordinates, (1, 2, 3))
211+
212+
213+
class TestEdge(test.bootstrap.IFC4):
214+
def test_run(self):
215+
builder = ShapeBuilder(self.file)
216+
edge = builder.edge((1, 0, 0), (1, 2, 3))
217+
assert np.allclose(edge.EdgeStart.VertexGeometry.Coordinates, (1, 0, 0))
218+
assert np.allclose(edge.EdgeEnd.VertexGeometry.Coordinates, (1, 2, 3))
219+
220+
221+
class TestFace(test.bootstrap.IFC4):
222+
def test_run(self):
223+
builder = ShapeBuilder(self.file)
224+
face = builder.face(((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)))
225+
assert np.allclose(face.Bounds[0].Bound.Polygon[0], (0, 0, 0))
226+
assert np.allclose(face.Bounds[0].Bound.Polygon[1], (1, 0, 0))
227+
assert np.allclose(face.Bounds[0].Bound.Polygon[2], (1, 1, 0))
228+
assert np.allclose(face.Bounds[0].Bound.Polygon[3], (0, 1, 0))
229+
230+
206231
class TestCalculateTransitions(test.bootstrap.IFC4):
207232
def calculate_and_test(self, params: dict[str, Any], length: Union[float, None]):
208233
np_X, np_Y = 0, 1

0 commit comments

Comments
 (0)