IFC4 Landscape Library

Created IFC library with simple trees, shrubs and hedges - with both 2d and 3d representations. Example - https://i.imgur.com/pOjh1si.png
Now it's available along with the furniture library when creating new project - https://i.imgur.com/Ho0B5Gn.png
This commit is contained in:
Andrej730
2024-01-24 11:47:19 +05:00
parent 7a7fde0a2a
commit 3fbd9d7cdf
4 changed files with 1431 additions and 9 deletions
@@ -25,18 +25,16 @@ from ifcopenshell.util.shape_builder import ShapeBuilder, V
class LibraryGenerator:
def generate(self, output_filename="IFC4 EU Steel.ifc"):
def generate(self, library_name, output_filename="IFC4 EU Steel.ifc"):
ifcopenshell.api.pre_listeners = {}
ifcopenshell.api.post_listeners = {}
self.materials = {}
self.file = ifcopenshell.api.run("project.create_file")
self.project = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class="IfcProject", name=f"Non-structural assets library"
)
self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name)
self.library = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=f"Non-structural assets library"
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
)
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
@@ -1876,4 +1874,6 @@ class LibraryGenerator:
if __name__ == "__main__":
path = Path(__file__).parents[1] / "blenderbim/bim/data/libraries"
LibraryGenerator().generate(output_filename=str(path / "IFC4 Furniture Library.ifc"))
LibraryGenerator().generate(
"Non-structural assets library", output_filename=str(path / "IFC4 Furniture Library.ifc")
)
@@ -0,0 +1,403 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 @Andrej730
#
# 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 ifcopenshell
import ifcopenshell.api
import random
from math import cos, sin, tan, pi
from pathlib import Path
from itertools import chain
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from collections import namedtuple
from mathutils import Vector, Matrix
from random import uniform
SimpleTreeParams = namedtuple("SimpleTreeParams", "plant_height crown_diameter trunk_diameter")
LowPolyTreeParams = namedtuple(
"LowPolyTreeParams", "plant_height crown_diameter crown_max_loc crown_taper trunk_height trunk_diameter random_seed"
)
PalmTreeParams = namedtuple("PalmTreeParams", "plant_height crown_diameter trunk_diameter")
TreePresetData = namedtuple("TreePresetData", "preset_class generator")
GeneratedGeometry = namedtuple("GeneratedGeometry", "items_2d items_3d")
def generate_low_poly_tree(
builder: ShapeBuilder,
crown_taper=0.5,
crown_max_loc=0.3,
plant_height=12.0,
trunk_height=2.0,
crown_diameter=4,
trunk_diameter=0.3,
random_seed=10,
) -> GeneratedGeometry:
unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file)
random.seed(random_seed)
res = 6
plant_height *= unit_conversion
trunk_height *= unit_conversion
crown_diameter *= unit_conversion
trunk_diameter *= unit_conversion
crown_height = plant_height - trunk_height
def circle(center, r, res):
points = []
for i in range(res):
# fmt: off
vert = (
cos(i * 2 * pi / res) * r + center[0],
sin(i * 2 * pi / res) * r + center[1],
0.0 + center[2]
)
# fmt: on
points.append(vert)
return points
def merge_lists(lists):
merged_list = []
for i in range(len(lists)):
merged_list = merged_list + lists[i]
return merged_list
def XY_scale(vert, scale):
vert_scaled = (vert[0] * scale, vert[1] * scale, vert[2])
return vert_scaled
def rand_add(vert, amp):
# fmt: off
vert_new = (
vert[0] + uniform(-amp, amp),
vert[1] + uniform(-amp, amp),
vert[2] + uniform(-amp, amp)
)
# fmt: on
return vert_new
height_segments_trunk = 2
height_segments_crown = 7
height_segments_total = height_segments_trunk + height_segments_crown
verts = []
# Trunk
for i in range(height_segments_trunk):
points = circle((0.0, 0.0, i * trunk_height), trunk_diameter / 2, res)
verts.append(points)
# Crown
h_crown_max_loc = int((height_segments_crown - 1) * crown_max_loc)
crown_indices = []
for i in range(0, h_crown_max_loc):
crown_indices.append(i)
for i in range(0, height_segments_crown - h_crown_max_loc):
crown_indices.append(h_crown_max_loc - i)
my_min_val = min(crown_indices)
my_max_val = max(crown_indices)
n_crown_taper = crown_taper * 0.8
crown_diameters = []
for x in crown_indices:
crown_diameters.append(((x - my_min_val) / (my_max_val - my_min_val)) * n_crown_taper + (1 - n_crown_taper))
randomize_amplitude = plant_height / 50
crown_segment_height = crown_height / height_segments_crown
for i in range(0, height_segments_crown - 1):
points = circle(
(0.0, 0.0, trunk_height + (i + 1) * crown_segment_height),
crown_diameter * crown_diameters[i] / 2,
res,
)
for i in range(0, len(points)):
points[i] = rand_add(points[i], randomize_amplitude)
verts.append(points)
# Top
verts.append(circle((0.0, 0.0, plant_height), 0.05, res))
edge_loops = []
for i in range(height_segments_total):
edge_loops.append(range(i * res, (i + 1) * res))
verts = merge_lists(verts)
faces = []
bottom = [*edge_loops[0]]
faces.append(bottom)
for i in range(height_segments_total - 1):
for j in range(res):
face = (edge_loops[i][j - 1], edge_loops[i][j], edge_loops[i + 1][j], edge_loops[i + 1][j - 1])
faces.append(face)
top = [*edge_loops[-1]]
faces.append(top)
verts_3D = [verts]
faces_3D = [faces]
verts_2D = [
[
(-0.5865642428398132, 0.8647078275680542, 0.0),
(-0.7583824992179871, 0.6572927236557007, 0.0),
(-0.7234422564506531, 0.5865941047668457, 0.0),
(-0.8714070916175842, 0.46631893515586853, 0.0),
(-0.9203394055366516, 0.15792329609394073, 0.0),
(-0.6487269997596741, 0.11522211134433746, 0.0),
(-0.9273074269294739, 0.0903107076883316, 0.0),
(-0.9606701731681824, -0.2852219343185425, 0.0),
(-0.8060722351074219, -0.35055163502693176, 0.0),
(-0.8271104693412781, -0.4677186608314514, 0.0),
(-0.6899875998497009, -0.6695915460586548, 0.0),
(-0.6215555667877197, -0.6767659187316895, 0.0),
(-0.4315463900566101, -0.8824808597564697, 0.0),
(-0.30316293239593506, -0.9023936986923218, 0.0),
(-0.32218849658966064, -0.8037619590759277, 0.0),
(-0.21792533993721008, -0.9530860185623169, 0.0),
(0.36917445063591003, -0.877334475517273, 0.0),
(0.30831706523895264, -0.685136079788208, 0.0),
(0.4778200685977936, -0.8094909191131592, 0.0),
(0.9163193106651306, -0.368840754032135, 0.0),
(0.827597439289093, -0.29377281665802, 0.0),
(0.9478662610054016, -0.245104119181633, 0.0),
(0.9667968153953552, 0.08458180725574493, 0.0),
(0.7529001832008362, 0.05212751030921936, 0.0),
(0.9102436900138855, 0.19098728895187378, 0.0),
(0.8279229998588562, 0.5522171258926392, 0.0),
(0.6635335087776184, 0.6461355686187744, 0.0),
(0.5753684639930725, 0.8170149326324463, 0.0),
(0.3976244330406189, 0.8779193162918091, 0.0),
(0.18733686208724976, 0.7513588666915894, 0.0),
(0.338344544172287, 0.910933256149292, 0.0),
(-0.013130240142345428, 0.9798095226287842, 0.0),
(-0.18649885058403015, 0.9414206743240356, 0.0),
(-0.3193224370479584, 0.8835403919219971, 0.0),
]
]
edges_2D = []
faces_2D = []
verts_2D_new = []
for vert in verts_2D[0]:
verts_2D_new.append(XY_scale(vert, crown_diameter / 2))
range_of_verts = range(0, len(verts_2D[0]))
for i in range_of_verts:
edges_2D.append([range_of_verts[i - 1], i])
faces_2D.append(i)
verts_2D[0] = verts_2D_new
edges_2D = [edges_2D]
faces_2D = [[faces_2D]]
verts_2D = [Vector(v).to_2d() for v in verts_2D[0]]
shape_2d = builder.polyline(points=verts_2D, closed=True)
shape_3d = builder.polygonal_face_set(points=verts_3D[0], faces=faces_3D[0])
return GeneratedGeometry((shape_2d,), (shape_3d,))
def generate_simple_tree(
builder: ShapeBuilder, plant_height=2, crown_diameter=2, trunk_diameter=0.1
) -> GeneratedGeometry:
unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file)
plant_height *= unit_conversion
crown_diameter *= unit_conversion
trunk_diameter *= unit_conversion
crown_radius = crown_diameter / 2
# 3d shape
trunk_base = builder.circle(radius=trunk_diameter / 2)
trunk = builder.extrude(trunk_base, magnitude=plant_height - crown_diameter)
crown_position = (0.0, 0.0, (plant_height - crown_radius))
crown = builder.sphere(radius=crown_radius, center=crown_position)
# 2d shape
shape_2D = builder.circle(radius=crown_radius)
return GeneratedGeometry((shape_2D,), (trunk, crown))
def generate_palm_tree(
builder: ShapeBuilder, plant_height=2, crown_diameter=5.8, trunk_diameter=0.1
) -> GeneratedGeometry:
unit_conversion = 1 / ifcopenshell.util.unit.calculate_unit_scale(builder.file)
palm_verts = [
Vector((-3.6415634155273438, -3.8275668621063232, -1.0948246717453003)),
Vector((-0.3698049783706665, -1.805966854095459, 0.493910551071167)),
Vector((-1.6397790908813477, -0.5056067705154419, 0.04353363811969757)),
Vector((0.0, 0.0, 0.0)),
Vector((5.023162364959717, -1.6531240940093994, -1.0948246717453003)),
Vector((1.8090602159500122, 0.4589407444000244, 0.493910551071167)),
Vector((1.1666079759597778, -1.2413609027862549, 0.04353363811969757)),
Vector((1.6942416429519653, 5.038582801818848, -1.0948246717453003)),
Vector((-0.43215513229370117, 1.833944320678711, 0.493910551071167)),
Vector((1.2652605772018433, 1.1839056015014648, 0.04353363811969757)),
Vector((-4.577561378479004, 2.689336061477661, -1.0948246717453003)),
Vector((-1.8646581172943115, -0.0367276668548584, 0.493910551071167)),
Vector((-0.8873085975646973, 1.4957730770111084, 0.04353363811969757)),
]
palm_faces = [(2, 1, 0), (2, 3, 1), (6, 5, 4), (6, 3, 5), (9, 8, 7), (9, 3, 8), (12, 11, 10), (12, 3, 11)]
# each segment starts with the last point from the previous
palm_segments_2d_ifc = [(3, 11, 10, 12, 3), (3, 1, 0, 2, 3), (3, 5, 4, 6, 3), (3, 8, 7, 9, 3)]
palm_segments_2d_ifc = [[i + 1 for i in segment] for segment in palm_segments_2d_ifc] # IFC indices start with 1
scale = Vector()
scale.xyz = crown_diameter / 11.6
leaves_height = 0.493910551071167 * scale.z
plant_height -= leaves_height
for v in palm_verts:
v *= scale
v.z += plant_height
v *= unit_conversion
plant_height *= unit_conversion
crown_diameter *= unit_conversion
trunk_diameter *= unit_conversion
# 3d
leaves = builder.polygonal_face_set(palm_verts, palm_faces)
trunk_base = builder.circle(radius=trunk_diameter / 2)
trunk = builder.extrude(trunk_base, magnitude=plant_height)
# 2d
remove_z = Vector((1, 1, 0))
verts_2D = [v * remove_z for v in palm_verts]
ifc_points = builder.file.createIfcCartesianPointList2D(verts_2D)
ifc_segments = []
for segment in palm_segments_2d_ifc:
ifc_segments.append(builder.file.createIfcLineIndex(segment))
shape_2d = builder.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return GeneratedGeometry((shape_2d,), (leaves, trunk))
tree_presets = {
"simple": TreePresetData(SimpleTreeParams, generate_simple_tree),
"low_poly": TreePresetData(LowPolyTreeParams, generate_low_poly_tree),
"palm_tree": TreePresetData(PalmTreeParams, generate_palm_tree),
}
class LibraryGenerator:
def generate(self, library_name, output_filename="IFC4 EU Steel.ifc"):
ifcopenshell.api.pre_listeners = {}
ifcopenshell.api.post_listeners = {}
self.materials = {}
self.file = ifcopenshell.api.run("project.create_file")
self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name)
self.library = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
)
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
)
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
self.representations = {
"model_body": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model,
),
"plan_body": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Plan",
context_identifier="Body",
target_view="PLAN_VIEW",
parent=plan,
),
}
self.builder = ShapeBuilder(self.file)
builder = self.builder
TreeData = namedtuple("TreeData", "tree_name preset_name preset_data")
trees = [
TreeData("Cone Tree (1.8m)", "low_poly", (1.8, 1.13, 0.0, 1.0, 0.28, 0.16, 10)),
TreeData("Cone Tree (15m)", "low_poly", (15.0, 4.37, 0.0, 1.0, 0.74, 0.44, 10)),
TreeData("Cone Tree (5m)", "low_poly", (5.0, 1.7, 0.0, 1.0, 0.32, 0.22, 10)),
TreeData("Hedge Small (0.4m)", "low_poly", (0.4, 0.4, 0.49, 0.15, 0.0, 0.0, 10)),
TreeData("Hedge Medium (0.8m)", "low_poly", (0.8, 0.8, 0.49, 0.15, 0.0, 0.0, 10)),
TreeData("Hedge Big (1.8m)", "low_poly", (1.8, 1.8, 0.49, 0.15, 0.0, 0.0, 10)),
TreeData("Palm Tree (5m)", "palm_tree", (5.0, 8.02, 0.2)),
TreeData("Palm Tree (15m)", "palm_tree", (15.0, 16.64, 0.4)),
TreeData("Shrub Small (0.4m)", "low_poly", (0.4, 0.4, 0.535, 0.524, 0.0, 0.0, 10)),
TreeData("Shrum Medium (0.8m)", "low_poly", (0.8, 0.8, 0.535, 0.524, 0.0, 0.0, 10)),
TreeData("Shrub Big (1.8m)", "low_poly", (1.8, 1.8, 0.535, 0.524, 0.0, 0.0, 10)),
TreeData("Simple Tree (1.8m)", "simple", (1.8, 1.34, 0.2)),
TreeData("Simple Tree (5m)", "simple", (5.0, 3.29, 0.2)),
TreeData("Simple Tree (15m)", "simple", (15.0, 9.21, 0.44)),
TreeData("Tree Small (1.8m)", "low_poly", (1.8, 1.7, 0.5, 0.36, 0.28, 0.16, 10)),
TreeData("Tree Big (15m)", "low_poly", (15.0, 10.0, 0.5, 0.36, 1.83, 0.44, 10)),
TreeData("Tree Medium (5m)", "low_poly", (5.0, 5.0, 0.5, 0.36, 0.84, 0.22, 10)),
]
for tree_data in trees:
preset = tree_presets.get(tree_data.preset_name)
preset_data = preset.preset_class(*tree_data.preset_data)._asdict()
tree_geometry = preset.generator(builder, **preset_data)
self.create_explicit_type(
"IfcGeographicElementType", tree_data.tree_name, **self.get_representations(tree_geometry)
)
self.file.write(output_filename)
def get_representations(self, generated_geometry: GeneratedGeometry):
representation_2d = self.builder.get_representation(
self.representations["plan_body"], items=generated_geometry.items_2d
)
representation_3d = self.builder.get_representation(
self.representations["model_body"], items=generated_geometry.items_3d
)
return {
"representation_3d": representation_3d,
"representation_2d": representation_2d,
}
def create_explicit_type(self, ifc_class, name, representation_3d, representation_2d, **params):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
for param, value in params.items():
setattr(element, param, value)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation_3d
)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation_2d
)
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
return element
if __name__ == "__main__":
path = Path(__file__).parents[1] / "blenderbim/bim/data/libraries"
LibraryGenerator().generate("Landscape Assets Library", output_filename=str(path / "IFC4 Landscape Library.ifc"))