Skip to content

Commit c6908f0

Browse files
committed
Merge branch 'modelgeometry'
2 parents 947096c + dc8f01b commit c6908f0

30 files changed

+775
-557
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added a base `BlockModel`.
13+
* Added reference to model `Element.model` to `Element`.
14+
* Added `Element.modelgeometry` as the cached geometry of an element in model coordinates, taking into account the modifying effect of interactions with other elements.
15+
* Added `Element.modeltransformation` as the cached transformation from element to model coordinates.
16+
* Added `Element.compute_elementgeometry()`.
17+
* Added `Element.compute_modelgeometry()` to replace `Element.compute_geometry()`.
18+
* Added `Element.compute_modeltransformation()` to replace `Element.compute_worldtransformation()`.
19+
1220
### Changed
1321

22+
* Changed `Element.graph_node` to `Element.graphnode`.
23+
* Changed `Element.tree_node` to `Element.treenode`.
24+
* Changed `blockmodel_interfaces` to use the bestfit frame shared by two aligned interfaces instead of the frame of first face of the pair.
25+
1426
### Removed
1527

28+
* Removed model reference `ElementTree.model` from `ElementTree`.
29+
* Removed `InterfaceElement` from elements.
30+
1631

1732
## [0.4.5] 2024-12-11
1833

@@ -35,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3550

3651
* Removed `compas_model.models.groupnode.GroupNode`.
3752

53+
3854
## [0.4.4] 2024-06-13
3955

4056
### Added

docs/_static/PLACEHOLDER

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/api/compas_model.interactions.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ compas_model.interactions
44

55
.. currentmodule:: compas_model.interactions
66

7+
This module provides classes for defining the type of interaction that exists between two elements.
8+
The interaction type could determine, for example, how forces are transferred from one element to the other.
9+
The interaction type could also determine whether an interaction is permanent or temporary;
10+
for example, for designing construction sequences.
11+
The different types of interactions will have to be interpreted by the context in which the model is used.
12+
13+
Interactions do not define the geometry of a joint or interface, but rather how the elements are connected.
14+
In the case of a wood joint, for example, an interaction could define whether the joinery is dry, glued, or mechanical,
15+
and what the properties of this connection are.
16+
717

818
Classes
919
=======

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ select = ["E", "F", "I"]
110110

111111
[tool.ruff.lint.isort]
112112
force-single-line = true
113+
known-first-party = [
114+
"compas",
115+
"compas_assembly",
116+
"compas_model",
117+
"compas_viewer",
118+
]
113119

114120
[tool.ruff.lint.pydocstyle]
115121
convention = "numpy"

scripts/test_blockmodel_arch.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33
import compas
44
from compas.colors import Color
55
from compas_assembly.geometry import Arch
6-
from compas_model.algorithms import blockmodel_interfaces
6+
from compas_viewer import Viewer
7+
8+
from compas_model.algorithms import model_interfaces
79
from compas_model.analysis import cra_penalty_solve
810
from compas_model.elements import BlockElement
911
from compas_model.interactions import ContactInterface
1012
from compas_model.models import Model
11-
from compas_viewer import Viewer
1213

1314
# =============================================================================
1415
# Block model
1516
# =============================================================================
1617

17-
template = Arch(rise=3, span=10, thickness=0.2, depth=0.5, n=30)
18+
template = Arch(rise=3, span=10, thickness=0.2, depth=0.5, n=200)
1819

1920
model = Model()
2021

@@ -25,13 +26,13 @@
2526
# Interfaces
2627
# =============================================================================
2728

28-
blockmodel_interfaces(model, amin=0.01)
29+
model_interfaces(model, amin=0.01)
2930

3031
# =============================================================================
3132
# Equilibrium
3233
# =============================================================================
3334

34-
elements: list[BlockElement] = sorted(model.elements(), key=lambda e: e.geometry.centroid().z)[:2]
35+
elements: list[BlockElement] = sorted(model.elements(), key=lambda e: e.modelgeometry.centroid.z)[:2]
3536

3637
for element in elements:
3738
element.is_support = True
@@ -61,7 +62,8 @@
6162
color = Color(0.8, 0.8, 0.8)
6263
show_faces = False
6364

64-
viewer.scene.add(element.geometry, show_points=False, show_faces=show_faces, facecolor=color)
65+
viewer.scene.add(element.modelgeometry, show_points=False, show_faces=show_faces, facecolor=color)
66+
6567

6668
for interaction in model.interactions():
6769
interaction: ContactInterface
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
from .collisions import is_aabb_aabb_collision
22
from .collisions import is_box_box_collision
33
from .collisions import is_face_to_face_collision
4-
from .collisions import get_collision_pairs
54

6-
from .interfaces import blockmodel_interfaces
5+
from .collisions import get_collision_pairs # rename to model_collisions
6+
from .interfaces import model_interfaces
7+
from .intersections import model_intersections
8+
from .overlaps import model_overlaps
79

810

911
__all__ = [
1012
"is_aabb_aabb_collision",
1113
"is_box_box_collision",
1214
"is_face_to_face_collision",
1315
"get_collision_pairs",
14-
"blockmodel_interfaces",
16+
"model_interfaces",
17+
"model_intersections",
18+
"model_overlaps",
1519
]

src/compas_model/algorithms/interfaces.py

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
11
from math import fabs
2-
from typing import List
32

43
from compas.datastructures import Mesh
54
from compas.geometry import Frame
65
from compas.geometry import Plane
76
from compas.geometry import Polygon
87
from compas.geometry import Transformation
8+
from compas.geometry import Vector
9+
from compas.geometry import bestfit_frame_numpy
910
from compas.geometry import centroid_polygon
1011
from compas.geometry import is_colinear
1112
from compas.geometry import is_coplanar
13+
from compas.geometry import is_parallel_vector_vector
1214
from compas.geometry import transform_points
1315
from compas.itertools import window
1416
from shapely.geometry import Polygon as ShapelyPolygon
1517

16-
# from compas_model.elements import BlockElement
1718
from compas_model.elements import BlockGeometry
1819
from compas_model.interactions import ContactInterface
1920
from compas_model.models import Model
2021

2122
from .nnbrs import find_nearest_neighbours
2223

2324

24-
def blockmodel_interfaces(
25+
def model_interfaces(
2526
model: Model,
2627
nmax: int = 10,
2728
tmax: float = 1e-6,
2829
amin: float = 1e-2,
2930
nnbrs_dims: int = 3,
30-
):
31+
) -> None:
3132
"""Identify the interfaces between the blocks of an assembly.
3233
3334
Parameters
3435
----------
35-
assembly : compas_assembly.datastructures.Assembly
36-
An assembly of discrete blocks.
36+
model : :class:`Model`
37+
A model containing blocks with mesh geometry.
3738
nmax : int, optional
3839
Maximum number of neighbours per block.
3940
tmax : float, optional
@@ -43,17 +44,21 @@ def blockmodel_interfaces(
4344
4445
Returns
4546
-------
46-
:class:`Assembly`
47+
None
48+
49+
Notes
50+
-----
51+
Interface planes are computed from the bestfit frame of the combined points of two faces.
4752
4853
"""
4954
node_index = {node: index for index, node in enumerate(model.graph.nodes())}
5055
index_node = {index: node for index, node in enumerate(model.graph.nodes())}
5156

52-
blocks: List[BlockGeometry] = [model.graph.node_element(node).geometry for node in model.graph.nodes()]
57+
blocks: list[BlockGeometry] = [model.graph.node_element(node).modelgeometry for node in model.graph.nodes()]
5358

5459
nmax = min(nmax, len(blocks))
5560

56-
block_cloud = [block.centroid() for block in blocks]
61+
block_cloud = [block.centroid for block in blocks]
5762
block_nnbrs = find_nearest_neighbours(block_cloud, nmax, dims=nnbrs_dims)
5863

5964
model.graph.edge = {node: {} for node in model.graph.nodes()}
@@ -68,71 +73,96 @@ def blockmodel_interfaces(
6873
n = index_node[j]
6974

7075
if n == node:
71-
# a block has no interfaces with itself
7276
continue
7377

7478
if model.graph.has_edge((n, node), directed=False):
75-
# the interfaces between these two blocks have already been identified
7679
continue
7780

7881
nbr = blocks[j]
7982

8083
interfaces = mesh_mesh_interfaces(block, nbr, tmax, amin)
8184

8285
if interfaces:
86+
# this can't be stored under interactions
87+
# it should be in an atteibute called "interfaces"
88+
# there can also be "collisions", "overlaps", "..."
8389
model.graph.add_edge(node, n, interactions=interfaces)
8490

85-
return model
86-
8791

8892
def mesh_mesh_interfaces(
89-
a: BlockGeometry,
90-
b: BlockGeometry,
93+
a: Mesh,
94+
b: Mesh,
9195
tmax: float = 1e-6,
9296
amin: float = 1e-1,
9397
) -> list[ContactInterface]:
9498
"""Compute all face-face contact interfaces between two meshes.
9599
96100
Parameters
97101
----------
98-
a : :class:`Block`
99-
b : :class:`Block`
102+
a : :class:`Mesh`
103+
The source mesh.
104+
b : :class:`Mesh`
105+
The target mesh.
100106
tmax : float, optional
101107
Maximum deviation from the perfectly flat interface plane.
102108
amin : float, optional
103109
Minimum area of a "face-face" interface.
104110
105111
Returns
106112
-------
107-
List[:class:`ContactInterface`]
113+
list[:class:`ContactInterface`]
114+
115+
Notes
116+
-----
117+
For equilibrium calculations with CRA, it is important that interface frames are aligned
118+
with the direction of the (interaction) edges on which they are stored.
119+
120+
This means that if the bestfit frame does not align with the normal of the base source frame,
121+
it will be inverted, such that it corresponds to whatever edge is created from this source to a target.
108122
109123
"""
110124
world = Frame.worldXY()
111-
interfaces = []
112-
frames = a.frames()
125+
interfaces: list[ContactInterface] = []
113126

114127
for face in a.faces():
115-
points = a.face_coordinates(face)
116-
frame = frames[face]
117-
matrix = Transformation.from_change_of_basis(world, frame)
118-
projected = transform_points(points, matrix)
119-
p0 = ShapelyPolygon(projected)
128+
a_points = a.face_coordinates(face)
129+
a_normal = a.face_normal(face)
120130

121131
for test in b.faces():
122-
points = b.face_coordinates(test)
123-
projected = transform_points(points, matrix)
124-
p1 = ShapelyPolygon(projected)
132+
b_points = b.face_coordinates(test)
133+
b_normal = b.face_normal(test)
134+
135+
if not is_parallel_vector_vector(a_normal, b_normal):
136+
continue
137+
138+
# this ensures that a shared frame is used to do the interface calculations
139+
# the frame should be oriented along the normal of the "a" face
140+
# this will align the interface frame with the resulting interaction edge
141+
# whgich is important for calculations with solvers such as CRA
142+
frame = Frame(*bestfit_frame_numpy(a_points + b_points))
143+
if frame.zaxis.dot(a_normal) < 0:
144+
frame.invert()
145+
146+
matrix = Transformation.from_change_of_basis(world, frame)
147+
148+
a_projected = transform_points(a_points, matrix)
149+
p0 = ShapelyPolygon(a_projected)
150+
151+
b_projected = transform_points(b_points, matrix)
152+
p1 = ShapelyPolygon(b_projected)
153+
154+
projected = a_projected + b_projected
125155

126156
if not all(fabs(point[2]) < tmax for point in projected):
127157
continue
128158

129-
if p1.area < amin:
159+
if p0.area < amin or p1.area < amin:
130160
continue
131161

132162
if not p0.intersects(p1):
133163
continue
134164

135-
intersection = p0.intersection(p1)
165+
intersection: ShapelyPolygon = p0.intersection(p1)
136166
area = intersection.area
137167

138168
if area < amin:
@@ -141,6 +171,10 @@ def mesh_mesh_interfaces(
141171
coords = [[x, y, 0.0] for x, y, _ in intersection.exterior.coords]
142172
coords = transform_points(coords, matrix.inverted())[:-1]
143173

174+
# this is not always an accurate representation of the interface
175+
# if the polygon has holes
176+
# the interface is incorrect
177+
144178
interface = ContactInterface(
145179
size=area,
146180
points=coords,
@@ -172,7 +206,7 @@ def merge_coplanar_interfaces(model: Model, tol: float = 1e-6) -> None:
172206
173207
"""
174208
for edge in model.graph.edges():
175-
interfaces: List[ContactInterface] = model.graph.edge_attribute(edge, "interfaces")
209+
interfaces: list[ContactInterface] = model.graph.edge_attribute(edge, "interfaces")
176210

177211
if interfaces:
178212
polygons = []
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def model_intersections():
2+
pass

src/compas_model/algorithms/nnbrs.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
1+
import numpy.typing as npt
12
from numpy import asarray
23
from scipy.spatial import cKDTree
34

45

5-
def find_nearest_neighbours(cloud, nmax, dims=3):
6+
def find_nearest_neighbours(cloud: npt.ArrayLike, nmax: int, dims: int = 3) -> list[tuple[list[float], list[int]]]:
7+
"""Find the nearest neighbours of each point in a cloud among all other points in the cloud.
8+
9+
Parameters
10+
----------
11+
cloud : array-like
12+
The cloud of points.
13+
nmax : int
14+
The maximum number of neighbours per point.
15+
dims : int, optional
16+
The number of dimensions to include in the search.
17+
18+
Results
19+
-------
20+
list[tuple[list[float], list[int]]]
21+
For each point, a tuple with the distances to the nearest neighbours,
22+
and the indices to the corresponding points.
23+
24+
"""
625
cloud = asarray(cloud)[:, :dims]
726
tree = cKDTree(cloud)
827
nnbrs = [tree.query(root, nmax) for root in cloud]

0 commit comments

Comments
 (0)