Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F80497351
utils.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
Sun, Sep 1, 01:36
Size
10 KB
Mime Type
text/x-python
Expires
Tue, Sep 3, 01:36 (2 d)
Engine
blob
Format
Raw Data
Handle
20385141
Attached To
R8831 BIMxBEM
utils.py
View Options
# coding: utf8
"""This module contains various utility functions not specific to another module.
© 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
typing
from
typing
import
Iterable
,
Any
,
Generator
,
List
import
FreeCAD
import
Part
from
freecad.bem.entities
import
Root
if
typing
.
TYPE_CHECKING
:
from
freecad.bem.typing
import
(
RelSpaceBoundaryFeature
,
)
# pylint: disable=no-name-in-module, import-error
TOLERANCE
=
0.001
def
append
(
doc_object
,
fc_property
,
value
:
Any
):
"""Intended to manipulate FreeCAD list like properties only"""
current_value
=
getattr
(
doc_object
,
fc_property
)
current_value
.
append
(
value
)
setattr
(
doc_object
,
fc_property
,
current_value
)
def
append_inner_wire
(
boundary
:
"RelSpaceBoundaryFeature"
,
wire
:
Part
.
Wire
)
->
None
:
"""Intended to manipulate FreeCAD list like properties only"""
outer_wire
=
get_outer_wire
(
boundary
)
inner_wires
=
get_inner_wires
(
boundary
)
inner_wires
.
append
(
wire
)
generate_boundary_compound
(
boundary
,
outer_wire
,
inner_wires
)
def
are_parallel_boundaries
(
boundary1
:
"RelSpaceBoundaryFeature"
,
boundary2
:
"RelSpaceBoundaryFeature"
)
->
bool
:
return
(
1
-
abs
(
get_boundary_normal
(
boundary1
)
.
dot
(
get_boundary_normal
(
boundary2
)))
<
TOLERANCE
)
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
clean_vectors
(
vectors
:
List
[
FreeCAD
.
Vector
])
->
None
:
"""Clean vectors for polygons creation
Keep only 1 point if 2 consecutive points are equal.
Remove point if it makes border go back and forth"""
i
=
0
while
i
<
len
(
vectors
):
pt1
=
vectors
[
i
-
1
]
pt2
=
vectors
[
i
]
pt3
=
vectors
[(
i
+
1
)
%
len
(
vectors
)]
if
are_3points_collinear
(
pt1
,
pt2
,
pt3
):
vectors
.
pop
(
i
)
i
=
i
-
1
if
i
>
0
else
0
continue
i
+=
1
def
close_vectors
(
vectors
:
List
[
FreeCAD
.
Vector
])
->
None
:
if
vectors
[
0
]
!=
vectors
[
-
1
]:
vectors
.
append
(
vectors
[
0
])
def
direction
(
vec0
:
FreeCAD
.
Vector
,
vec1
:
FreeCAD
.
Vector
)
->
FreeCAD
.
Vector
:
return
(
vec0
-
vec1
)
.
normalize
()
def
generate_boundary_compound
(
boundary
:
"RelSpaceBoundaryFeature"
,
outer_wire
:
Part
.
Wire
,
inner_wires
:
List
[
Part
.
Wire
],
):
"""Generate boundary compound composed of 1 Face, 1 OuterWire, 0-n InnerWires"""
face
=
Part
.
Face
(
outer_wire
)
for
inner_wire
in
inner_wires
:
new_face
=
face
.
cut
(
Part
.
Face
(
inner_wire
))
if
not
new_face
.
Area
:
b_id
=
(
boundary
.
Id
if
isinstance
(
boundary
.
Proxy
,
Root
)
else
boundary
.
SourceBoundary
.
Id
)
raise
RuntimeError
(
f
"""Failure. An inner_wire did not cut face correctly in boundary <{b_id}>.
OuterWire area = {Part.Face(outer_wire).Area / 10 ** 6},
InnerWire area = {Part.Face(inner_wire).Area / 10 ** 6}"""
)
face
=
new_face
boundary
.
Shape
=
Part
.
Compound
([
face
,
outer_wire
,
*
inner_wires
])
def
get_axis_by_name
(
placement
,
name
):
axis_dict
=
{
"AXIS1"
:
0
,
"AXIS2"
:
1
,
"AXIS3"
:
2
}
return
FreeCAD
.
Vector
(
placement
.
Matrix
.
A
[
axis_dict
[
name
]
:
12
:
4
])
def
get_by_id
(
ifc_id
:
int
,
elements
:
Iterable
[
Part
.
Feature
])
->
Part
.
Feature
:
for
element
in
elements
:
try
:
if
element
.
Id
==
ifc_id
:
return
element
except
AttributeError
:
continue
def
get_by_class
(
doc
=
FreeCAD
.
ActiveDocument
,
by_class
=
object
):
"""Generator throught FreeCAD document element of specific python proxy class"""
for
element
in
doc
.
Objects
:
try
:
if
isinstance
(
element
.
Proxy
,
by_class
):
yield
element
except
AttributeError
:
continue
def
get_elements_by_ifctype
(
ifc_type
:
str
,
doc
=
FreeCAD
.
ActiveDocument
)
->
Generator
[
Part
.
Feature
,
None
,
None
]:
"""Generator throught FreeCAD document element of specific ifc_type"""
for
element
in
doc
.
Objects
:
try
:
if
element
.
IfcType
==
ifc_type
:
yield
element
except
(
AttributeError
,
ReferenceError
):
continue
def
get_boundaries_by_element
(
element
:
Part
.
Feature
,
doc
)
->
List
[
Part
.
Feature
]:
return
[
boundary
for
boundary
in
get_elements_by_ifctype
(
"IfcRelSpaceBoundary"
,
doc
)
if
boundary
.
RelatedBuildingElement
==
element
]
def
get_boundaries_by_element_id
(
element_id
,
doc
):
return
(
boundary
for
boundary
in
doc
.
Objects
if
getattr
(
getattr
(
boundary
,
"RelatedBuilding"
,
None
),
"Id"
,
None
)
==
element_id
)
def
get_face_ref_point
(
face
):
center_of_mass
=
face
.
CenterOfMass
if
face
.
isInside
(
center_of_mass
,
TOLERANCE
,
True
):
return
center_of_mass
pt1
=
face
.
distToShape
(
Part
.
Vertex
(
center_of_mass
))[
1
][
0
][
0
]
line
=
Part
.
Line
(
center_of_mass
,
pt1
)
plane
=
Part
.
Plane
(
center_of_mass
,
face
.
normalAt
(
0
,
0
))
intersections
=
[
line
.
intersect2d
(
e
.
Curve
,
plane
)
for
e
in
face
.
Edges
]
intersections
=
[
i
[
0
]
for
i
in
intersections
if
i
]
if
not
len
(
intersections
)
==
2
:
intersections
=
sorted
(
intersections
)[
0
:
2
]
mid_param
=
tuple
(
sum
(
params
)
/
len
(
params
)
for
params
in
zip
(
*
intersections
))
return
plane
.
value
(
*
mid_param
)
def
get_element_by_guid
(
guid
,
elements_group
):
for
fc_element
in
getattr
(
elements_group
,
"Group"
,
elements_group
):
if
getattr
(
fc_element
,
"GlobalId"
,
None
)
==
guid
:
return
fc_element
raise
LookupError
(
f
"""Unable to get element by {guid}.
This error is known to occurs when you model 2 parallel walls instead of a multilayer wall."""
)
def
get_host_guid
(
ifc_entity
):
return
(
ifc_entity
.
FillsVoids
[
0
]
.
RelatingOpeningElement
.
VoidsElements
[
0
]
.
RelatingBuildingElement
.
GlobalId
)
def
get_in_list_by_id
(
elements
,
element_id
):
if
element_id
==
-
1
:
return
None
for
element
in
elements
:
if
element
.
Id
==
element_id
:
return
element
raise
LookupError
(
f
"No element with Id <{element_id}> found"
)
def
get_face_normal
(
face
,
at_point
=
None
)
->
FreeCAD
.
Vector
:
at_point
=
at_point
or
face
.
Vertexes
[
0
]
.
Point
params
=
face
.
Surface
.
projectPoint
(
at_point
,
"LowerDistanceParameters"
)
return
face
.
normalAt
(
*
params
)
def
get_boundary_normal
(
fc_boundary
,
at_point
=
None
)
->
FreeCAD
.
Vector
:
return
get_face_normal
(
fc_boundary
.
Shape
.
Faces
[
0
],
at_point
)
def
get_plane
(
fc_boundary
)
->
Part
.
Plane
:
"""Intended for RelSpaceBoundary use only"""
vertexes
=
get_outer_wire
(
fc_boundary
)
.
Vertexes
vec1
,
vec2
=
[
v
.
Point
for
v
in
vertexes
[
0
:
2
]]
for
vtx
in
vertexes
[
2
:]:
try
:
return
Part
.
Plane
(
vec1
,
vec2
,
vtx
.
Point
)
except
Part
.
OCCError
:
continue
def
get_area_from_points
(
points
:
List
[
FreeCAD
.
Vector
])
->
float
:
"""Return area considering points are consecutive points of a polygon
Return 0 for invalid polygons"""
clean_vectors
(
points
)
close_vectors
(
points
)
try
:
return
Part
.
Face
(
Part
.
makePolygon
(
points
))
.
Area
except
Part
.
OCCError
:
return
0
def
get_vectors_from_shape
(
shape
:
Part
.
Shape
):
return
[
vx
.
Point
for
vx
in
shape
.
Vertexes
]
def
get_boundary_outer_vectors
(
boundary
):
return
[
vx
.
Point
for
vx
in
get_outer_wire
(
boundary
)
.
Vertexes
]
def
get_outer_wire
(
boundary
):
return
[
s
for
s
in
boundary
.
Shape
.
SubShapes
if
isinstance
(
s
,
Part
.
Wire
)][
0
]
def
get_inner_wires
(
boundary
):
return
[
s
for
s
in
boundary
.
Shape
.
SubShapes
if
isinstance
(
s
,
Part
.
Wire
)][
1
:]
def
get_wires
(
boundary
:
Part
.
Feature
)
->
Generator
[
Part
.
Wire
,
None
,
None
]:
return
(
s
for
s
in
boundary
.
Shape
.
SubShapes
if
isinstance
(
s
,
Part
.
Wire
))
def
are_edges_parallel
(
edge1
:
Part
.
Edge
,
edge2
:
Part
.
Edge
)
->
bool
:
dir1
=
line_from_edge
(
edge1
)
.
Direction
dir2
=
line_from_edge
(
edge2
)
.
Direction
return
abs
(
dir1
.
dot
(
dir2
))
>
1
-
TOLERANCE
def
is_coplanar
(
boundary1
,
boundary2
):
"""Intended for RelSpaceBoundary use only
For some reason native Part.Shape.isCoplanar(Part.Shape) do not always work"""
plane1
=
get_plane
(
boundary1
)
plane2
=
get_plane
(
boundary2
)
same_dir
=
plane1
.
Axis
.
dot
(
plane2
.
Axis
)
>
1
-
TOLERANCE
p2_on_plane
=
(
# Strangely distanceToPlane can be negative
abs
(
plane2
.
Position
.
distanceToPlane
(
plane1
.
Position
,
plane1
.
Axis
))
<
TOLERANCE
)
return
same_dir
and
p2_on_plane
def
line_from_edge
(
edge
:
Part
.
Edge
)
->
Part
.
Line
:
points
=
[
v
.
Point
for
v
in
edge
.
Vertexes
]
return
Part
.
Line
(
*
points
)
def
polygon_from_lines
(
lines
,
base_plane
):
new_points
=
[]
for
li1
,
line1
in
enumerate
(
lines
):
li2
=
li1
-
1
line2
=
lines
[
li2
]
# Need to ensure direction are not same to avoid crash in OCCT 7.4
if
abs
(
line1
.
Direction
.
dot
(
line2
.
Direction
))
>=
1
-
TOLERANCE
:
continue
new_points
.
append
(
base_plane
.
value
(
*
line1
.
intersect2d
(
line2
,
base_plane
)[
0
]))
clean_vectors
(
new_points
)
if
len
(
new_points
)
<
3
:
raise
ShapeCreationError
close_vectors
(
new_points
)
return
Part
.
makePolygon
(
new_points
)
def
project_wire_to_plane
(
wire
,
plane
)
->
Part
.
Wire
:
new_vectors
=
[
v
.
Point
.
projectToPlane
(
plane
.
Position
,
plane
.
Axis
)
for
v
in
wire
.
Vertexes
]
close_vectors
(
new_vectors
)
return
Part
.
makePolygon
(
new_vectors
)
def
project_boundary_onto_plane
(
boundary
,
plane
:
Part
.
Plane
):
outer_wire
=
get_outer_wire
(
boundary
)
inner_wires
=
get_inner_wires
(
boundary
)
outer_wire
=
project_wire_to_plane
(
outer_wire
,
plane
)
inner_wires
=
[
project_wire_to_plane
(
wire
,
plane
)
for
wire
in
inner_wires
]
generate_boundary_compound
(
boundary
,
outer_wire
,
inner_wires
)
def
remove_inner_wire
(
boundary
,
wire
)
->
None
:
if
wire
in
boundary
.
Shape
.
Wires
:
boundary
.
Shape
=
boundary
.
Shape
.
removeShape
([
wire
])
return
area
=
Part
.
Face
(
wire
)
.
Area
for
inner_wire
in
get_inner_wires
(
boundary
):
if
abs
(
Part
.
Face
(
inner_wire
)
.
Area
-
area
)
<
TOLERANCE
:
boundary
.
Shape
=
boundary
.
Shape
.
removeShape
([
inner_wire
])
return
def
update_boundary_shape
(
boundary
)
->
None
:
outer_wire
=
get_outer_wire
(
boundary
)
inner_wires
=
get_inner_wires
(
boundary
)
generate_boundary_compound
(
boundary
,
outer_wire
,
inner_wires
)
def
are_3points_collinear
(
pt1
:
FreeCAD
.
Vector
,
pt2
:
FreeCAD
.
Vector
,
pt3
:
FreeCAD
.
Vector
)
->
bool
:
for
vec1
,
vec2
in
itertools
.
combinations
((
pt1
,
pt2
,
pt3
),
2
):
if
vec1
.
isEqual
(
vec2
,
TOLERANCE
):
return
True
dir1
=
vectors_dir
(
pt1
,
pt2
)
dir2
=
vectors_dir
(
pt2
,
pt3
)
return
dir1
.
isEqual
(
dir2
,
TOLERANCE
)
or
dir1
.
isEqual
(
-
dir2
,
TOLERANCE
)
def
vectors_dir
(
pt1
:
FreeCAD
.
Vector
,
pt2
:
FreeCAD
.
Vector
)
->
FreeCAD
.
Vector
:
return
(
pt2
-
pt1
)
.
normalize
()
class
IsTooSmall
(
BaseException
):
pass
class
ShapeCreationError
(
RuntimeError
):
pass
Event Timeline
Log In to Comment