Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F62723420
boundaries.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Wed, May 15, 03:52
Size
33 KB
Mime Type
text/x-python
Expires
Fri, May 17, 03:52 (2 d)
Engine
blob
Format
Raw Data
Handle
17681417
Attached To
R8831 BIMxBEM
boundaries.py
View Options
# coding: utf8
"""This module adapt IfcRelSpaceBoundary and create SIA specific bem boundaries in FreeCAD.
© All rights reserved.
ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, Laboratory CNPA, 2019-2020
See the LICENSE.TXT file for more details.
Author : Cyril Waechter
"""
import
itertools
import
os
from
collections
import
namedtuple
import
typing
from
typing
import
NamedTuple
,
Iterable
,
List
import
ifcopenshell
import
ifcopenshell.geom
import
ifcopenshell.util.element
import
ifcopenshell.util.unit
import
FreeCAD
import
FreeCADGui
import
Part
from
freecad.bem
import
materials
from
freecad.bem.bem_xml
import
BEMxml
from
freecad.bem.bem_logging
import
logger
,
LOG_STREAM
from
freecad.bem
import
utils
from
freecad.bem.entities
import
(
RelSpaceBoundary
,
BEMBoundary
,
Element
,
)
from
freecad.bem.ifc_importer
import
IfcImporter
,
TOLERANCE
if
typing
.
TYPE_CHECKING
:
from
freecad.bem.typing
import
(
SpaceFeature
,
ContainerFeature
,
)
# pylint: disable=no-name-in-module, import-error
def
processing_sia_boundaries
(
doc
=
FreeCAD
.
ActiveDocument
)
->
None
:
"""Create SIA specific boundaries cf. https://www.sia.ch/fr/services/sia-norm/"""
for
space
in
utils
.
get_elements_by_ifctype
(
"IfcSpace"
,
doc
):
ensure_hosted_element_are
(
space
)
ensure_hosted_are_coplanar
(
space
)
compute_space_area
(
space
)
set_boundary_normal
(
space
)
join_over_splitted_boundaries
(
space
,
doc
)
handle_curtain_walls
(
space
,
doc
)
find_closest_edges
(
space
)
set_leso_type
(
space
)
ensure_external_earth_is_set
(
space
,
doc
)
create_sia_boundaries
(
doc
)
doc
.
recompute
()
def
ensure_external_earth_is_set
(
space
:
"SpaceFeature"
,
doc
=
FreeCAD
.
ActiveDocument
):
sites
:
List
[
"ContainerFeature"
]
=
list
(
utils
.
get_elements_by_ifctype
(
"IfcSite"
,
doc
)
)
ground_bound_box
=
get_ground_bound_box
(
sites
)
if
space
.
Shape
.
BoundBox
.
ZMin
-
ground_bound_box
.
ZMax
>
1000
:
return
ground_shape
=
Part
.
Compound
([])
for
site
in
sites
:
ground_shape
.
add
(
site
.
Shape
)
if
not
ground_shape
.
BoundBox
.
isValid
():
ground_shape
=
Part
.
Plane
()
.
toShape
()
for
boundary
in
space
.
SecondLevel
.
Group
:
if
boundary
.
InternalOrExternalBoundary
in
(
"INTERNAL"
,
"EXTERNAL_EARTH"
,
"EXTERNAL_WATER"
,
"EXTERNAL_FIRE"
,
):
continue
if
not
is_underground
(
boundary
,
ground_shape
):
continue
boundary
.
InternalOrExternalBoundary
=
"EXTERNAL_EARTH"
def
is_underground
(
boundary
,
ground_shape
)
->
bool
:
closest_points
=
ground_shape
.
distToShape
(
boundary
.
Shape
)[
1
][
0
]
direction
:
FreeCAD
.
Vector
=
closest_points
[
1
]
-
closest_points
[
0
]
if
direction
.
z
>
1000
:
return
False
if
boundary
.
LesoType
==
"Flooring"
:
el_thickness
=
getattr
(
getattr
(
getattr
(
boundary
,
"RelatedBuildingElement"
,
0
),
"Thickness"
,
0
),
"Value"
,
0
,
)
if
direction
.
z
-
el_thickness
*
1.5
>
0
:
return
False
boundary
.
UndergroundDepth
=
abs
(
direction
.
z
-
el_thickness
)
return
True
if
boundary
.
LesoType
==
"Wall"
:
bbox
=
boundary
.
Shape
.
BoundBox
if
(
bbox
.
ZMax
+
bbox
.
ZMin
)
/
2
+
direction
.
z
<
0
:
return
True
if
boundary
.
LesoType
==
"Ceiling"
:
if
direction
.
z
<
TOLERANCE
:
return
True
return
False
def
get_ground_bound_box
(
sites
:
Iterable
[
"ContainerFeature"
])
->
FreeCAD
.
BoundBox
:
boundbox
=
FreeCAD
.
BoundBox
()
for
site
in
sites
:
boundbox
.
add
(
site
.
Shape
.
BoundBox
)
return
boundbox
if
boundbox
.
isValid
()
else
FreeCAD
.
BoundBox
(
0
,
0
,
-
30000
,
0
,
0
,
0
)
def
set_boundary_normal
(
space
):
faces
=
space
.
Shape
.
Faces
for
boundary
in
space
.
SecondLevel
.
Group
:
if
boundary
.
IsHosted
:
continue
center_of_mass
=
utils
.
get_outer_wire
(
boundary
)
.
CenterOfMass
face
=
min
(
faces
,
key
=
lambda
x
:
x
.
Surface
.
projectPoint
(
center_of_mass
,
"LowerDistance"
)
)
face_normal
=
face
.
normalAt
(
*
face
.
Surface
.
projectPoint
(
center_of_mass
,
"LowerDistanceParameters"
)
)
normal
=
utils
.
get_normal_at
(
boundary
)
if
normal
.
dot
(
face_normal
)
<
0
:
normal
=
-
normal
boundary
.
Normal
=
normal
for
hosted
in
boundary
.
InnerBoundaries
:
hosted
.
Normal
=
normal
def
compute_space_area
(
space
:
Part
.
Feature
):
"""Compute both gross and net area"""
z_min
=
space
.
Shape
.
BoundBox
.
ZMin
z_sre
=
z_min
+
1000
# 1 m above ground. See SIA 380:2015 &3.2.3 p.26-27
sre_plane
=
Part
.
Plane
(
FreeCAD
.
Vector
(
0
,
0
,
z_sre
),
FreeCAD
.
Vector
(
0
,
0
,
1
))
space
.
Area
=
space
.
Shape
.
common
(
sre_plane
.
toShape
())
.
Area
# TODO: Not valid yet as it return net area. Find a way to get gross space volume
space
.
AreaAE
=
space
.
Area
def
handle_curtain_walls
(
space
,
doc
)
->
None
:
"""Add an hosted window with full area in curtain wall boundaries as they are not handled
by BEM softwares"""
for
boundary
in
space
.
SecondLevel
.
Group
:
if
getattr
(
boundary
.
RelatedBuildingElement
,
"IfcType"
,
""
)
!=
"IfcCurtainWall"
:
continue
fake_window
=
doc
.
copyObject
(
boundary
)
fake_window
.
IsHosted
=
True
fake_window
.
LesoType
=
"Window"
fake_window
.
ParentBoundary
=
boundary
.
Id
fake_window
.
GlobalId
=
ifcopenshell
.
guid
.
new
()
fake_window
.
Id
=
IfcId
.
new
(
doc
)
RelSpaceBoundary
.
set_label
(
fake_window
)
space
.
SecondLevel
.
addObject
(
fake_window
)
# Host cannot be an empty face so inner wire is scaled down a little
inner_wire
=
utils
.
get_outer_wire
(
boundary
)
.
scale
(
0.999
)
inner_wire
=
utils
.
project_wire_to_plane
(
inner_wire
,
utils
.
get_plane
(
boundary
))
utils
.
append_inner_wire
(
boundary
,
inner_wire
)
utils
.
append
(
boundary
,
"InnerBoundaries"
,
fake_window
)
if
FreeCAD
.
GuiUp
:
fake_window
.
ViewObject
.
ShapeColor
=
(
0.0
,
0.7
,
1.0
)
class
IfcId
:
"""Generate new id for generated boundaries missing from ifc and keep track of last id used"""
current_id
=
0
@classmethod
def
new
(
cls
,
doc
)
->
int
:
if
not
cls
.
current_id
:
cls
.
current_id
=
max
((
getattr
(
obj
,
"Id"
,
0
)
for
obj
in
doc
.
Objects
))
cls
.
current_id
+=
1
return
cls
.
current_id
def
write_xml
(
doc
=
FreeCAD
.
ActiveDocument
)
->
BEMxml
:
"""Read BEM infos for FreeCAD file and write it to an xml.
xml is stored in an object to allow different outputs"""
bem_xml
=
BEMxml
()
for
project
in
utils
.
get_elements_by_ifctype
(
"IfcProject"
,
doc
):
bem_xml
.
write_project
(
project
)
for
space
in
utils
.
get_elements_by_ifctype
(
"IfcSpace"
,
doc
):
bem_xml
.
write_space
(
space
)
for
boundary
in
space
.
SecondLevel
.
Group
:
bem_xml
.
write_boundary
(
boundary
)
for
building_element
in
utils
.
get_by_class
(
doc
,
Element
):
bem_xml
.
write_building_elements
(
building_element
)
for
material
in
utils
.
get_by_class
(
doc
,
(
materials
.
Material
,
materials
.
ConstituentSet
,
materials
.
LayerSet
)
):
bem_xml
.
write_material
(
material
)
return
bem_xml
def
output_xml_to_path
(
bem_xml
,
xml_path
=
None
):
if
not
xml_path
:
xml_path
=
(
"./output.xml"
if
os
.
name
==
"nt"
else
"/home/cyril/git/BIMxBEM/output.xml"
)
bem_xml
.
write_to_file
(
xml_path
)
def
join_over_splitted_boundaries
(
space
,
doc
=
FreeCAD
.
ActiveDocument
):
boundaries
=
space
.
SecondLevel
.
Group
# Considered as the minimal size for an oversplit to occur (1 ceiling, 3 wall, 1 flooring)
if
len
(
boundaries
)
<=
5
:
return
elements_dict
=
dict
()
for
rel_boundary
in
boundaries
:
try
:
key
=
f
"{rel_boundary.RelatedBuildingElement.Id}_{rel_boundary.InternalOrExternalBoundary}"
except
AttributeError
:
if
rel_boundary
.
PhysicalOrVirtualBoundary
==
"VIRTUAL"
:
logger
.
info
(
"IfcElement
%s
is VIRTUAL. Modeling error ?"
)
key
=
"VIRTUAL"
else
:
logger
.
warning
(
"IfcElement
%s
has no RelatedBuildingElement"
,
rel_boundary
.
Id
)
corresponding_boundary
=
rel_boundary
.
CorrespondingBoundary
if
corresponding_boundary
:
key
+=
str
(
corresponding_boundary
.
Id
)
elements_dict
.
setdefault
(
key
,
[])
.
append
(
rel_boundary
)
for
key
,
boundary_list
in
elements_dict
.
items
():
# None coplanar boundaries should not be connected.
# eg. round wall splitted with multiple orientations.
# Case1: No oversplitted boundaries
if
len
(
boundary_list
)
==
1
:
continue
coplanar_boundaries
=
list
([])
for
boundary
in
boundary_list
:
if
not
coplanar_boundaries
:
coplanar_boundaries
.
append
([
boundary
])
continue
for
coplanar_list
in
coplanar_boundaries
:
# TODO: Test if this test is not too strict considering precision
if
utils
.
is_coplanar
(
boundary
,
coplanar_list
[
0
]):
coplanar_list
.
append
(
boundary
)
break
else
:
coplanar_boundaries
.
append
([
boundary
])
for
coplanar_list
in
coplanar_boundaries
:
# Case 1 : only 1 boundary related to the same element. Cannot group boundaries.
if
len
(
coplanar_list
)
==
1
:
continue
# Case 2 : more than 1 boundary related to the same element might be grouped.
try
:
join_coplanar_boundaries
(
coplanar_list
,
doc
)
except
Part
.
OCCError
:
logger
.
warning
(
f
"Cannot join boundaries in space <{space.Id}> with key <{key}>"
)
class
CommonSegment
(
NamedTuple
):
index1
:
int
index2
:
int
opposite_dir
:
FreeCAD
.
Vector
def
join_coplanar_boundaries
(
boundaries
:
list
,
doc
=
FreeCAD
.
ActiveDocument
):
"""Try to join coplanar boundaries"""
boundary1
=
boundaries
.
pop
()
remove_from_doc
=
list
()
def
find_common_segment
(
wire1
,
wire2
):
"""Find if wires have common segments and between which edges
return named tuple with edge index from each wire and if they have opposite direction"""
for
(
ei1
,
edge1
),
(
ei2
,
edge2
)
in
itertools
.
product
(
enumerate
(
wire1
.
Edges
),
enumerate
(
wire2
.
Edges
)
):
if
wire1
==
wire2
and
ei1
==
ei2
:
continue
common_segment
=
edges_have_common_segment
(
edge1
,
edge2
)
if
common_segment
:
return
CommonSegment
(
ei1
,
ei2
,
common_segment
.
opposite_dir
)
def
edges_have_common_segment
(
edge1
,
edge2
):
"""Check if edges have common segments and tell if these segments have same direction"""
p0_1
,
p0_2
=
utils
.
get_vectors_from_shape
(
edge1
)
p1_1
,
p1_2
=
utils
.
get_vectors_from_shape
(
edge2
)
v0_12
=
p0_2
-
p0_1
v1_12
=
p1_2
-
p1_1
dir0
=
(
v0_12
)
.
normalize
()
dir1
=
(
v1_12
)
.
normalize
()
# if edge1 and edge2 are not collinear no junction is possible.
if
not
(
(
dir0
.
isEqual
(
dir1
,
TOLERANCE
)
or
dir0
.
isEqual
(
-
dir1
,
TOLERANCE
))
and
v0_12
.
cross
(
p1_1
-
p0_1
)
.
Length
<
TOLERANCE
):
return
# Check in which order vectors1 and vectors2 should be connected
if
dir0
.
isEqual
(
dir1
,
TOLERANCE
):
p0_1_next_point
,
other_point
=
p1_1
,
p1_2
opposite_dir
=
False
else
:
p0_1_next_point
,
other_point
=
p1_2
,
p1_1
opposite_dir
=
True
# Check if edge1 and edge2 have a common segment
if
not
(
dir0
.
dot
(
p0_1_next_point
-
p0_1
)
<
dir0
.
dot
(
p0_2
-
p0_1
)
and
dir0
.
negative
()
.
dot
(
other_point
-
p0_2
)
<
dir0
.
negative
()
.
dot
(
p0_1
-
p0_2
)
):
return
return
CommonSegment
(
None
,
None
,
opposite_dir
)
def
join_boundaries
(
boundary1
,
boundary2
):
wire1
=
utils
.
get_outer_wire
(
boundary1
)
vectors1
=
utils
.
get_vectors_from_shape
(
wire1
)
wire2
=
utils
.
get_outer_wire
(
boundary2
)
vectors2
=
utils
.
get_vectors_from_shape
(
wire2
)
common_segment
=
find_common_segment
(
wire1
,
wire2
)
if
not
common_segment
:
return
False
ei1
,
ei2
,
opposite_dir
=
common_segment
# join vectors1 and vectors2 at indexes
new_points
=
vectors2
[
ei2
+
1
:]
+
vectors2
[:
ei2
+
1
]
if
not
opposite_dir
:
new_points
.
reverse
()
# Efficient way to insert elements at index : https://stackoverflow.com/questions/14895599/insert-an-element-at-specific-index-in-a-list-and-return-updated-list/48139870#48139870 pylint: disable=line-too-long
vectors1
[
ei1
+
1
:
ei1
+
1
]
=
new_points
inner_wires
=
utils
.
get_inner_wires
(
boundary1
)[:]
inner_wires
.
extend
(
utils
.
get_inner_wires
(
boundary2
))
if
not
boundary1
.
IsHosted
:
for
inner_boundary
in
boundary2
.
InnerBoundaries
:
utils
.
append
(
boundary1
,
"InnerBoundaries"
,
inner_boundary
)
inner_boundary
.
ParentBoundary
=
boundary1
.
Id
# Update shape
utils
.
clean_vectors
(
vectors1
)
utils
.
close_vectors
(
vectors1
)
wire1
=
Part
.
makePolygon
(
vectors1
)
utils
.
generate_boundary_compound
(
boundary1
,
wire1
,
inner_wires
)
RelSpaceBoundary
.
recompute_areas
(
boundary1
)
return
True
while
True
:
for
boundary2
in
boundaries
:
if
join_boundaries
(
boundary1
,
boundary2
):
boundaries
.
remove
(
boundary2
)
remove_from_doc
.
append
(
boundary2
)
break
else
:
logger
.
warning
(
f
"""Unable to join boundaries RelSpaceBoundary Id <{boundary1.Id}>
with boundaries <{(b.Id for b in boundaries)}>"""
)
break
wire1
=
utils
.
get_outer_wire
(
boundary1
)
vectors1
=
utils
.
get_vectors_from_shape
(
wire1
)
inner_wires
=
utils
.
get_inner_wires
(
boundary1
)[:]
while
True
:
common_segment
=
find_common_segment
(
wire1
,
wire1
)
if
not
common_segment
:
break
ei1
,
ei2
=
common_segment
[
0
:
2
]
# join vectors1 and vectors2 at indexes
vectors_split1
=
vectors1
[:
ei1
+
1
]
+
vectors1
[
ei2
+
1
:]
vectors_split2
=
vectors1
[
ei1
+
1
:
ei2
+
1
]
utils
.
clean_vectors
(
vectors_split1
)
utils
.
clean_vectors
(
vectors_split2
)
area1
=
Part
.
Face
(
Part
.
makePolygon
(
vectors_split1
+
[
vectors_split1
[
0
]]))
.
Area
area2
=
Part
.
Face
(
Part
.
makePolygon
(
vectors_split2
+
[
vectors_split2
[
0
]]))
.
Area
if
area1
>
area2
:
vectors1
=
vectors_split1
inner_vectors
=
vectors_split2
else
:
vectors1
=
vectors_split2
inner_vectors
=
vectors_split1
utils
.
close_vectors
(
inner_vectors
)
inner_polygon
=
Part
.
makePolygon
(
inner_vectors
)
if
Part
.
Face
(
inner_polygon
)
.
Area
>
TOLERANCE
:
inner_wires
.
extend
([
inner_polygon
])
# Update shape
utils
.
close_vectors
(
vectors1
)
wire1
=
Part
.
makePolygon
(
vectors1
)
utils
.
generate_boundary_compound
(
boundary1
,
wire1
,
inner_wires
)
RelSpaceBoundary
.
recompute_areas
(
boundary1
)
# Clean FreeCAD document if join operation was a success
for
fc_object
in
remove_from_doc
:
doc
.
removeObject
(
fc_object
.
Name
)
def
ensure_hosted_element_are
(
space
):
for
boundary
in
space
.
SecondLevel
.
Group
:
try
:
ifc_type
=
boundary
.
RelatedBuildingElement
.
IfcType
except
AttributeError
:
continue
if
not
is_typically_hosted
(
ifc_type
):
continue
if
boundary
.
IsHosted
and
boundary
.
ParentBoundary
:
continue
def
are_too_far
(
boundary1
,
boundary2
):
max_distance
=
getattr
(
getattr
(
boundary2
.
RelatedBuildingElement
,
"Thickness"
,
0
),
"Value"
,
0
)
return
(
boundary1
.
Shape
.
distToShape
(
boundary2
.
Shape
)[
0
]
-
max_distance
>
TOLERANCE
)
def
find_host
(
boundary
):
fallback_solution
=
None
for
boundary2
in
space
.
SecondLevel
.
Group
:
if
boundary
is
boundary2
:
continue
if
not
utils
.
are_parallel_boundaries
(
boundary
,
boundary2
):
continue
if
are_too_far
(
boundary
,
boundary2
):
continue
fallback_solution
=
boundary2
for
inner_wire
in
utils
.
get_inner_wires
(
boundary2
):
if
(
not
abs
(
Part
.
Face
(
inner_wire
)
.
Area
-
boundary
.
Area
.
Value
)
<
TOLERANCE
):
continue
return
boundary2
if
not
fallback_solution
:
raise
HostNotFound
(
f
"No host found for RelSpaceBoundary Id<{boundary.Id}>"
)
logger
.
warning
(
f
"Using fallback solution to resolve host of RelSpaceBoundary Id<{boundary.Id}>"
)
return
fallback_solution
try
:
host
=
find_host
(
boundary
)
except
HostNotFound
as
err
:
logger
.
exception
(
err
)
boundary
.
IsHosted
=
True
boundary
.
ParentBoundary
=
host
.
Id
utils
.
append
(
host
,
"InnerBoundaries"
,
boundary
)
def
ensure_hosted_are_coplanar
(
space
):
for
boundary
in
space
.
SecondLevel
.
Group
:
for
inner_boundary
in
boundary
.
InnerBoundaries
:
if
utils
.
is_coplanar
(
inner_boundary
,
boundary
):
continue
utils
.
project_boundary_onto_plane
(
inner_boundary
,
utils
.
get_plane
(
boundary
))
outer_wire
=
utils
.
get_outer_wire
(
boundary
)
inner_wires
=
utils
.
get_inner_wires
(
boundary
)
inner_wire
=
utils
.
get_outer_wire
(
inner_boundary
)
inner_wires
.
append
(
inner_wire
)
try
:
face
=
boundary
.
Shape
.
Faces
[
0
]
face
=
face
.
cut
(
Part
.
Face
(
inner_wire
))
except
RuntimeError
:
pass
boundary
.
Shape
=
Part
.
Compound
([
face
,
outer_wire
,
*
inner_wires
])
def
is_typically_hosted
(
ifc_type
:
str
):
"""Say if given ifc_type is typically hosted eg. windows, doors"""
usually_hosted_types
=
(
"IfcWindow"
,
"IfcDoor"
,
"IfcOpeningElement"
)
for
usual_type
in
usually_hosted_types
:
if
ifc_type
.
startswith
(
usual_type
):
return
True
return
False
class
HostNotFound
(
LookupError
):
pass
Closest
=
namedtuple
(
"Closest"
,
[
"boundary"
,
"edge"
,
"distance"
])
def
init_closest_default_values
(
boundaries
):
for
boundary
in
boundaries
:
n_edges
=
len
(
utils
.
get_outer_wire
(
boundary
)
.
Edges
)
boundary
.
Proxy
.
closest
=
[
Closest
(
boundary
=
None
,
edge
=-
1
,
distance
=
100000
)
]
*
n_edges
def
compare_closest_edges
(
boundary1
,
ei1
,
edge1
,
boundary2
,
ei2
,
edge2
):
distance
=
boundary1
.
Proxy
.
closest
[
ei1
]
.
distance
edge_to_edge
=
edge_distance_to_edge
(
edge1
,
edge2
)
if
distance
<=
TOLERANCE
:
return
elif
edge_to_edge
<=
TOLERANCE
or
edge_to_edge
-
distance
-
TOLERANCE
<=
0
:
boundary1
.
Proxy
.
closest
[
ei1
]
=
Closest
(
boundary2
,
ei2
,
edge_to_edge
)
def
find_closest_by_distance
(
boundary1
,
boundary2
):
edges1
=
utils
.
get_outer_wire
(
boundary1
)
.
Edges
edges2
=
utils
.
get_outer_wire
(
boundary2
)
.
Edges
for
(
ei1
,
edge1
),
(
ei2
,
edge2
)
in
itertools
.
product
(
enumerate
(
edges1
),
enumerate
(
edges2
)
):
if
not
is_low_angle
(
edge1
,
edge2
):
continue
compare_closest_edges
(
boundary1
,
ei1
,
edge1
,
boundary2
,
ei2
,
edge2
)
compare_closest_edges
(
# pylint: disable=arguments-out-of-order
boundary2
,
ei2
,
edge2
,
boundary1
,
ei1
,
edge1
)
def
find_closest_by_intersection
(
boundary1
,
boundary2
):
intersect_line
=
utils
.
get_plane
(
boundary1
)
.
intersectSS
(
utils
.
get_plane
(
boundary2
))[
0
]
edges1
=
utils
.
get_outer_wire
(
boundary1
)
.
Edges
edges2
=
utils
.
get_outer_wire
(
boundary2
)
.
Edges
for
(
ei1
,
edge1
),
(
ei2
,
edge2
)
in
itertools
.
product
(
enumerate
(
edges1
),
enumerate
(
edges2
)
):
is_closest
=
False
distance1
=
edge_distance_to_line
(
edge1
,
intersect_line
)
distance2
=
edge_distance_to_line
(
edge2
,
intersect_line
)
min_distance
=
boundary1
.
Proxy
.
closest
[
ei1
]
.
distance
if
distance1
<
min_distance
:
boundary1
.
Proxy
.
closest
[
ei1
]
=
Closest
(
boundary2
,
-
1
,
distance1
)
min_distance
=
boundary2
.
Proxy
.
closest
[
ei2
]
.
distance
if
distance2
<
min_distance
:
boundary2
.
Proxy
.
closest
[
ei2
]
=
Closest
(
boundary1
,
-
1
,
distance2
)
def
find_closest_edges
(
space
:
"SpaceFeature"
)
->
None
:
"""Find closest boundary and edge to be able to reconstruct a closed shell"""
boundaries
=
[
b
for
b
in
space
.
SecondLevel
.
Group
if
not
b
.
IsHosted
]
init_closest_default_values
(
boundaries
)
# Loop through all boundaries and edges to find the closest edge
for
boundary1
,
boundary2
in
itertools
.
combinations
(
boundaries
,
2
):
# If boundary1 and boundary2 have opposite direction no match possible
normals_dot
=
boundary2
.
Normal
.
dot
(
boundary1
.
Normal
)
if
normals_dot
<=
-
1
+
TOLERANCE
:
continue
# If boundaries are not almost parallel, they must intersect
if
not
normals_dot
>=
1
-
TOLERANCE
:
find_closest_by_intersection
(
boundary1
,
boundary2
)
# If they are parallel all edges need to be compared
else
:
find_closest_by_distance
(
boundary1
,
boundary2
)
# Store found values in standard FreeCAD properties
for
boundary
in
boundaries
:
closest_boundaries
,
boundary
.
ClosestEdges
,
closest_distances
=
(
list
(
i
)
for
i
in
zip
(
*
boundary
.
Proxy
.
closest
)
)
boundary
.
ClosestBoundaries
=
[
b
.
Id
if
b
else
-
1
for
b
in
closest_boundaries
]
boundary
.
ClosestDistance
=
[
int
(
d
)
for
d
in
closest_distances
]
def
set_leso_type
(
space
):
for
boundary
in
space
.
SecondLevel
.
Group
:
boundary
.
LesoType
=
define_leso_type
(
boundary
)
def
define_leso_type
(
boundary
):
try
:
ifc_type
=
boundary
.
RelatedBuildingElement
.
IfcType
except
AttributeError
:
if
boundary
.
PhysicalOrVirtualBoundary
!=
"VIRTUAL"
:
logger
.
warning
(
f
"Unable to define LesoType for boundary <{boundary.Id}>"
)
return
"Unknown"
if
ifc_type
.
startswith
(
"IfcWindow"
):
return
"Window"
elif
ifc_type
.
startswith
(
"IfcDoor"
):
return
"Door"
elif
ifc_type
.
startswith
(
"IfcWall"
):
return
"Wall"
elif
ifc_type
.
startswith
(
"IfcSlab"
)
or
ifc_type
==
"IfcRoof"
:
# Pointing up => Ceiling. Pointing down => Flooring
if
boundary
.
Normal
.
z
>
0
:
return
"Ceiling"
return
"Flooring"
elif
ifc_type
.
startswith
(
"IfcOpeningElement"
):
return
"Opening"
else
:
logger
.
warning
(
f
"Unable to define LesoType for Boundary Id <{boundary.Id}>"
)
return
"Unknown"
def
edge_distance_to_edge
(
edge1
:
Part
.
Edge
,
edge2
:
Part
.
Edge
)
->
float
:
mid_point
=
edge1
.
CenterOfMass
line_segment
=
(
v
.
Point
for
v
in
edge2
.
Vertexes
)
return
mid_point
.
distanceToLineSegment
(
*
line_segment
)
.
Length
def
edge_distance_to_line
(
edge
,
line
):
mid_point
=
edge
.
CenterOfMass
return
mid_point
.
distanceToLine
(
line
.
Location
,
line
.
Direction
)
def
is_low_angle
(
edge1
,
edge2
):
dir1
=
(
edge1
.
Vertexes
[
1
]
.
Point
-
edge1
.
Vertexes
[
0
]
.
Point
)
.
normalize
()
dir2
=
(
edge2
.
Vertexes
[
1
]
.
Point
-
edge2
.
Vertexes
[
0
]
.
Point
)
.
normalize
()
return
abs
(
dir1
.
dot
(
dir2
))
>
0.5
# Low angle considered as < 30°. cos(pi/3)=0.5.
def
create_sia_boundaries
(
doc
=
FreeCAD
.
ActiveDocument
):
"""Create boundaries necessary for SIA calculations"""
project
=
next
(
utils
.
get_elements_by_ifctype
(
"IfcProject"
,
doc
))
is_from_revit
=
project
.
ApplicationIdentifier
==
"Revit"
for
space
in
utils
.
get_elements_by_ifctype
(
"IfcSpace"
,
doc
):
create_sia_ext_boundaries
(
space
,
is_from_revit
)
create_sia_int_boundaries
(
space
,
is_from_revit
)
rejoin_boundaries
(
space
,
"SIA_Exterior"
)
rejoin_boundaries
(
space
,
"SIA_Interior"
)
def
rejoin_boundaries
(
space
,
sia_type
):
"""
Rejoin boundaries after their translation to get a correct close shell surfaces.
1 Fill gaps between boundaries (2b)
2 Fill gaps gerenate by translation to make a boundary on the inside or outside boundary of
building elements
https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/schema/ifcproductextension/lexical/ifcrelspaceboundary2ndlevel.htm # pylint: disable=line-too-long
"""
base_boundaries
=
space
.
SecondLevel
.
Group
for
base_boundary
in
base_boundaries
:
lines
=
[]
boundary1
=
getattr
(
base_boundary
,
sia_type
)
if
(
base_boundary
.
IsHosted
or
base_boundary
.
PhysicalOrVirtualBoundary
==
"VIRTUAL"
or
not
base_boundary
.
RelatedBuildingElement
):
continue
b1_plane
=
utils
.
get_plane
(
boundary1
)
for
b2_id
,
(
ei1
,
ei2
)
in
zip
(
base_boundary
.
ClosestBoundaries
,
enumerate
(
base_boundary
.
ClosestEdges
)
):
base_boundary2
=
utils
.
get_in_list_by_id
(
base_boundaries
,
b2_id
)
boundary2
=
getattr
(
base_boundary2
,
sia_type
,
None
)
if
not
boundary2
:
logger
.
warning
(
f
"Cannot find corresponding boundary with id <{b2_id}>"
)
lines
.
append
(
utils
.
line_from_edge
(
utils
.
get_outer_wire
(
base_boundary
)
.
Edges
[
ei1
])
)
continue
# Case 1 : boundaries are not parallel
plane_intersect
=
b1_plane
.
intersectSS
(
utils
.
get_plane
(
boundary2
))
if
plane_intersect
:
lines
.
append
(
plane_intersect
[
0
])
continue
# Case 2 : boundaries are parallel
line1
=
utils
.
line_from_edge
(
utils
.
get_outer_wire
(
boundary1
)
.
Edges
[
ei1
])
try
:
line2
=
utils
.
line_from_edge
(
utils
.
get_outer_wire
(
boundary2
)
.
Edges
[
ei2
])
except
IndexError
:
logger
.
warning
(
f
"""Cannot find closest edge index <{ei2}> in boundary id <{b2_id}>
to rejoin boundary <{base_boundary.Id}>"""
)
lines
.
append
(
utils
.
line_from_edge
(
utils
.
get_outer_wire
(
base_boundary
)
.
Edges
[
ei1
])
)
continue
# Case 2a : edges are not parallel
if
abs
(
line1
.
Direction
.
dot
(
line2
.
Direction
))
<
1
-
TOLERANCE
:
line_intersect
=
line1
.
intersect2d
(
line2
,
b1_plane
)
if
line_intersect
:
point1
=
b1_plane
.
value
(
*
line_intersect
[
0
])
if
line1
.
Direction
.
dot
(
line2
.
Direction
)
>
0
:
point2
=
point1
+
line1
.
Direction
+
line2
.
Direction
else
:
point2
=
point1
+
line1
.
Direction
-
line2
.
Direction
continue
# Case 2b : edges are parallel
else
:
point1
=
(
line1
.
Location
+
line2
.
Location
)
*
0.5
point2
=
point1
+
line1
.
Direction
try
:
lines
.
append
(
Part
.
Line
(
point1
,
point2
))
except
Part
.
OCCError
:
logger
.
exception
(
f
"Failure in boundary id <{base_boundary.Id}> {point1} and {point2} are equal"
)
# Generate new shape
try
:
outer_wire
=
utils
.
polygon_from_lines
(
lines
,
b1_plane
)
except
utils
.
NoIntersectionError
:
# TODO: Investigate to see why this happens
logger
.
exception
(
f
"Unable to rejoin boundary Id <{base_boundary.Id}>"
)
continue
except
Part
.
OCCError
:
logger
.
exception
(
f
"Invalid geometry while rejoining boundary Id <{base_boundary.Id}>"
)
continue
try
:
Part
.
Face
(
outer_wire
)
except
Part
.
OCCError
:
logger
.
exception
(
f
"Unable to rejoin boundary Id <{base_boundary.Id}>"
)
continue
inner_wires
=
utils
.
get_inner_wires
(
boundary1
)
try
:
utils
.
generate_boundary_compound
(
boundary1
,
outer_wire
,
inner_wires
)
except
RuntimeError
as
err
:
logger
.
exception
(
err
)
continue
boundary1
.
Area
=
area
=
boundary1
.
Shape
.
Area
for
inner_boundary
in
base_boundary
.
InnerBoundaries
:
area
=
area
+
inner_boundary
.
Shape
.
Area
boundary1
.
AreaWithHosted
=
area
def
create_sia_ext_boundaries
(
space
,
is_from_revit
):
"""Create SIA boundaries from RelSpaceBoundaries and translate it if necessary"""
sia_group_obj
=
space
.
Boundaries
.
newObject
(
"App::DocumentObjectGroup"
,
"SIA_Exteriors"
)
space
.
SIA_Exteriors
=
sia_group_obj
for
boundary1
in
space
.
SecondLevel
.
Group
:
if
boundary1
.
IsHosted
or
boundary1
.
PhysicalOrVirtualBoundary
==
"VIRTUAL"
:
continue
bem_boundary
=
BEMBoundary
.
create
(
boundary1
,
"SIA_Exterior"
)
sia_group_obj
.
addObject
(
bem_boundary
)
if
not
boundary1
.
RelatedBuildingElement
:
continue
thickness
=
boundary1
.
RelatedBuildingElement
.
Thickness
.
Value
leso_type
=
boundary1
.
LesoType
normal
=
boundary1
.
Normal
# EXTERNAL: there is multiple possible values for external so testing internal is better.
if
boundary1
.
InternalOrExternalBoundary
!=
"INTERNAL"
:
distance
=
thickness
if
is_from_revit
and
leso_type
==
"Wall"
:
distance
/=
2
bem_boundary
.
Placement
.
move
(
normal
*
distance
)
# INTERNAL. TODO: Check during tests if NOTDEFINED case need to be handled ?
else
:
if
leso_type
==
"Flooring"
:
continue
if
leso_type
==
"Ceiling"
:
if
is_from_revit
:
continue
distance
=
thickness
else
:
# Walls
if
is_from_revit
:
continue
distance
=
thickness
/
2
bem_boundary
.
Placement
.
move
(
normal
*
distance
)
def
create_sia_int_boundaries
(
space
,
is_from_revit
):
"""Create boundaries necessary for SIA calculations"""
sia_group_obj
=
space
.
Boundaries
.
newObject
(
"App::DocumentObjectGroup"
,
"SIA_Interiors"
)
space
.
SIA_Interiors
=
sia_group_obj
for
boundary
in
space
.
SecondLevel
.
Group
:
if
boundary
.
IsHosted
or
boundary
.
PhysicalOrVirtualBoundary
==
"VIRTUAL"
:
continue
normal
=
boundary
.
Normal
bem_boundary
=
BEMBoundary
.
create
(
boundary
,
"SIA_Interior"
)
sia_group_obj
.
addObject
(
bem_boundary
)
if
not
boundary
.
RelatedBuildingElement
:
continue
leso_type
=
boundary
.
LesoType
if
is_from_revit
:
if
leso_type
==
"Flooring"
:
continue
thickness
=
boundary
.
RelatedBuildingElement
.
Thickness
.
Value
move_dir
=
-
normal
if
leso_type
==
"Wall"
:
distance
=
thickness
/
2
if
leso_type
==
"Ceiling"
and
boundary
.
InternalOrExternalBoundary
==
"INTERNAL"
:
distance
=
thickness
bem_boundary
.
Placement
.
move
(
move_dir
*
distance
)
class
XmlResult
(
NamedTuple
):
xml
:
str
log
:
str
def
generate_bem_xml_from_file
(
ifc_path
:
str
)
->
XmlResult
:
ifc_importer
=
IfcImporter
(
ifc_path
)
ifc_importer
.
generate_rel_space_boundaries
()
doc
=
ifc_importer
.
doc
processing_sia_boundaries
(
doc
)
xml_str
=
write_xml
(
doc
)
.
tostring
()
log_str
=
LOG_STREAM
.
getvalue
()
return
XmlResult
(
xml_str
,
log_str
)
def
process_test_file
(
ifc_path
,
doc
):
ifc_importer
=
IfcImporter
(
ifc_path
,
doc
)
ifc_importer
.
generate_rel_space_boundaries
()
processing_sia_boundaries
(
doc
)
bem_xml
=
write_xml
(
doc
)
output_xml_to_path
(
bem_xml
)
ifc_importer
.
xml
=
bem_xml
ifc_importer
.
log
=
LOG_STREAM
.
getvalue
()
if
FreeCAD
.
GuiUp
:
FreeCADGui
.
activeView
()
.
viewIsometric
()
FreeCADGui
.
SendMsgToActiveView
(
"ViewFit"
)
with
open
(
"./boundaries.log"
,
"w"
,
encoding
=
"utf-8"
)
as
log_file
:
log_file
.
write
(
ifc_importer
.
log
)
return
ifc_importer
if
__name__
==
"__main__"
:
if
os
.
name
==
"nt"
:
TEST_FOLDER
=
r"C:\git\BIMxBEM\IfcTestFiles"
else
:
TEST_FOLDER
=
"/home/cyril/git/BIMxBEM/IfcTestFiles/"
TEST_FILES
=
{
0
:
"Triangle_A24_IFC4.ifc"
,
1
:
"Triangle_2x3_R19.ifc"
,
2
:
"2Storey_2x3_A22.ifc"
,
3
:
"2Storey_2x3_R19.ifc"
,
4
:
"0014_Vernier112D_ENE_ModèleÉnergétique_R20.ifc"
,
7
:
"OverSplitted_R20_2x3.ifc"
,
8
:
"3196 Aalseth Lane_R21_bem.ifc"
,
9
:
"ExternalEarth_R20_IFC4.ifc"
,
10
:
"Ersatzneubau Alphütte_1-1210_31_23.ifc"
,
11
:
"GRAPHISOFT_ARCHICAD_Sample_Project_Hillside_House_v1.ifczip"
,
12
:
"GRAPHISOFT_ARCHICAD_Sample_Project_S_Office_v1.ifczip"
,
13
:
"Cas1_EXPORT_REVIT_IFC2x3 (EDITED)_Space_Boundaries.ifc"
,
14
:
"Cas1_EXPORT_REVIT_IFC4DTV (EDITED)_Space_Boundaries.ifc"
,
15
:
"Cas1_EXPORT_REVIT_IFC4RV (EDITED)_Space_Boundaries.ifc"
,
16
:
"Cas1_EXPORT_REVIT_IFC4RV (EDITED)_Space_Boundaries_RECREATED.ifc"
,
17
:
"Cas2_EXPORT_REVIT_IFC4RV (EDITED)_Space_Boudaries.ifc"
,
18
:
"Cas2_EXPORT_REVIT_IFC4DTV (EDITED)_Space_Boundaries_RECREATED.ifc"
,
19
:
"Cas2_EXPORT_REVIT_IFC4DTV (EDITED)_Space_Boundaries.ifc"
,
20
:
"Cas2_EXPORT_REVIT_IFC2x3 (EDITED)_Space_Boundaries.ifc"
,
21
:
"Temoin.ifc"
,
22
:
"1708 maquette test 01.ifc"
,
23
:
"test 02-03 mur int baseslab dalle de sol.ifc"
,
24
:
"test 02-06 murs composites.ifc"
,
25
:
"test 02-07 dalle étage et locaux mansardés.ifc"
,
26
:
"test 02-08 raccords nettoyés étage.ifc"
,
27
:
"test 02-09 sous-sol.ifc"
,
28
:
"test 02-02 mur matériau simple.ifc"
,
29
:
"3196 Aalseth Lane_R19_bem.ifc"
,
30
:
"Maison Privée.ifc"
,
}
IFC_PATH
=
os
.
path
.
join
(
TEST_FOLDER
,
TEST_FILES
[
8
])
DOC
=
FreeCAD
.
ActiveDocument
if
DOC
:
# Remote debugging
import
ptvsd
# Allow other computers to attach to ptvsd at this IP address and port.
ptvsd
.
enable_attach
(
address
=
(
"localhost"
,
5678
),
redirect_output
=
True
)
# Pause the program until a remote debugger is attached
ptvsd
.
wait_for_attach
()
# breakpoint()
process_test_file
(
IFC_PATH
,
DOC
)
else
:
FreeCADGui
.
showMainWindow
()
DOC
=
FreeCAD
.
newDocument
()
process_test_file
(
IFC_PATH
,
DOC
)
# xml_str = generate_bem_xml_from_file(IFC_PATH)
FreeCADGui
.
exec_loop
()
Event Timeline
Log In to Comment