Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Added `compas_model.elements.Element.parent` as alias for `compas_model.elements.Element.tree_node.parent`.
* Added missing graph node reference to elements during deserialisation process.
* Added a base `BlockModel`.
* Added reference to model `Element.model` to `Element`.
* Added `Element.modelgeometry` as the cached geometry of an element in model coordinates, taking into account the modifying effect of interactions with other elements.
* Added `Element.modeltransformation` as the cached transformation from element to model coordinates.
* Added `Element.compute_elementgeometry()`.
* Added `Element.compute_modelgeometry()` to replace `Element.compute_geometry()`.
* Added `Element.compute_modeltransformation()` to replace `Element.compute_worldtransformation()`.
* Added `Element.is_dirty` that is changed together with neighbor elements.
* Added tests for element attributes.

### Changed

Expand All @@ -29,11 +38,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Changed `compas_model.elements.Element.compute_worldtransformation` to include frame of model.
* Changed `compas_model.models.elementnode.ElementNode` to include children (previous functionality of `GroupNode`).
* Changed root of element tree to element node instead of group node.
* Changed deserialisation process of model according to removal of group node.
* Changed deserialisation process of model according to removal of group node.\
* Changed `Element.graph_node` to `Element.graphnode`.
* Changed `Element.tree_node` to `Element.treenode`.
* Changed `blockmodel_interfaces` to use the bestfit frame shared by two aligned interfaces instead of the frame of first face of the pair.

### Removed

* Removed `compas_model.models.groupnode.GroupNode`.
* Removed model reference `ElementTree.model` from `ElementTree`.
* Removed `InterfaceElement` from elements.

## [0.4.4] 2024-06-13

Expand Down
5 changes: 5 additions & 0 deletions docs/examples/grid_model/000_one_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Example file to do the following:
# Create a model with 4 columns, 4 column-heads and 1 plate.
# Compute interactions between the elements. TODO: Add interactions and the base element level when geometry is computed.
# Change one column head and check if the elements are dirty.
# Recompute the modelgeometry.
Copy link
Collaborator Author

@petrasvestartas petrasvestartas Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file will be used to draft a simple model where one element is changed by another to trigger "is_dirty" attribute for current and neighbours_out elements.

Later this example will be changed to a test file.

10 changes: 6 additions & 4 deletions scripts/test_blockmodel_arch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
import compas
from compas.colors import Color
from compas_assembly.geometry import Arch
from compas_viewer import Viewer

from compas_model.algorithms import blockmodel_interfaces
from compas_model.analysis import cra_penalty_solve
from compas_model.elements import BlockElement
from compas_model.interactions import ContactInterface
from compas_model.models import Model
from compas_viewer import Viewer

# =============================================================================
# Block model
# =============================================================================

template = Arch(rise=3, span=10, thickness=0.2, depth=0.5, n=30)
template = Arch(rise=3, span=10, thickness=0.2, depth=0.5, n=200)

model = Model()

Expand All @@ -31,7 +32,7 @@
# Equilibrium
# =============================================================================

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

for element in elements:
element.is_support = True
Expand Down Expand Up @@ -61,7 +62,8 @@
color = Color(0.8, 0.8, 0.8)
show_faces = False

viewer.scene.add(element.geometry, show_points=False, show_faces=show_faces, facecolor=color)
viewer.scene.add(element.modelgeometry, show_points=False, show_faces=show_faces, facecolor=color)


for interaction in model.interactions():
interaction: ContactInterface
Expand Down
58 changes: 46 additions & 12 deletions src/compas_model/algorithms/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@
from compas.geometry import Plane
from compas.geometry import Polygon
from compas.geometry import Transformation
from compas.geometry import Vector
from compas.geometry import bestfit_frame_numpy
from compas.geometry import centroid_polygon
from compas.geometry import is_colinear
from compas.geometry import is_coplanar
from compas.geometry import is_parallel_vector_vector
from compas.geometry import transform_points
from compas.itertools import window
from shapely.geometry import Polygon as ShapelyPolygon

# from compas_model.elements import BlockElement
from compas_model.elements import BlockGeometry
from compas_model.interactions import ContactInterface
from compas_model.models import Model

from .nnbrs import find_nearest_neighbours


def invert(self):
self._yaxis = self._yaxis * -1
self._zaxis = self._zaxis * -1


Frame.invert = invert


def blockmodel_interfaces(
model: Model,
nmax: int = 10,
Expand Down Expand Up @@ -49,7 +59,7 @@ def blockmodel_interfaces(
node_index = {node: index for index, node in enumerate(model.graph.nodes())}
index_node = {index: node for index, node in enumerate(model.graph.nodes())}

blocks: List[BlockGeometry] = [model.graph.node_element(node).geometry for node in model.graph.nodes()]
blocks: List[BlockGeometry] = [model.graph.node_element(node).modelgeometry for node in model.graph.nodes()]

nmax = min(nmax, len(blocks))

Expand Down Expand Up @@ -106,27 +116,51 @@ def mesh_mesh_interfaces(
-------
List[:class:`ContactInterface`]

Notes
-----
For equilibrium calculations with CRA, it is important that interface frames are aligned
with the direction of the (interaction) edges on which they are stored.

This means that if the

"""
world = Frame.worldXY()
interfaces = []
frames = a.frames()
# frames = a.frames()

for face in a.faces():
points = a.face_coordinates(face)
frame = frames[face]
matrix = Transformation.from_change_of_basis(world, frame)
projected = transform_points(points, matrix)
p0 = ShapelyPolygon(projected)
a_points = a.face_coordinates(face)
a_normal = a.face_normal(face)

for test in b.faces():
points = b.face_coordinates(test)
projected = transform_points(points, matrix)
p1 = ShapelyPolygon(projected)
b_points = b.face_coordinates(test)
b_normal: Vector = b.face_normal(test)

if not is_parallel_vector_vector(a_normal, b_normal):
continue

# this ensures that a shared frame is used to do the interface calculations
# the frame should be oriented along the normal of the "a" face
# this will align the interface frame with the resulting interaction edge
# whgich is important for calculations with solvers such as CRA
frame = Frame(*bestfit_frame_numpy(a_points + b_points))
if frame.zaxis.dot(a_normal) < 0:
frame.invert()

matrix = Transformation.from_change_of_basis(world, frame)

a_projected = transform_points(a_points, matrix)
p0 = ShapelyPolygon(a_projected)

b_projected = transform_points(b_points, matrix)
p1 = ShapelyPolygon(b_projected)

projected = a_projected + b_projected

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

if p1.area < amin:
if p0.area < amin or p1.area < amin:
continue

if not p0.intersects(p1):
Expand Down
4 changes: 2 additions & 2 deletions src/compas_model/analysis/cra.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ def cra_penalty_solve(
element_block = {}

for element in model.elements():
block: Block = element.geometry.copy(cls=Block)
block: Block = element.modelgeometry.copy(cls=Block)
x, y, z = block.centroid()
node = assembly.add_block(block, x=x, y=y, z=z, is_support=element.is_support)
element_block[element.graph_node] = node
element_block[element.graphnode] = node

for edge in model.graph.edges():
interactions: list[ContactInterface] = model.graph.edge_interactions(edge)
Expand Down
4 changes: 0 additions & 4 deletions src/compas_model/elements/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from .block import BlockElement
from .block import BlockFeature
from .block import BlockGeometry
from .interface import InterfaceElement
from .interface import InterfaceFeature
from .plate import PlateElement
from .plate import PlateFeature

Expand All @@ -17,8 +15,6 @@
"BlockElement",
"BlockFeature",
"BlockGeometry",
"InterfaceElement",
"InterfaceFeature",
"PlateElement",
"PlateFeature",
]
104 changes: 58 additions & 46 deletions src/compas_model/elements/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from compas.geometry import Box
from compas.geometry import Frame
from compas.geometry import Point
from compas.geometry import bestfit_frame_numpy
from compas.geometry import bounding_box
from compas.geometry import centroid_points
from compas.geometry import centroid_polyhedron
from compas.geometry import cross_vectors
from compas.geometry import dot_vectors
from compas.geometry import oriented_bounding_box
from compas.geometry import volume_polyhedron
Expand All @@ -15,30 +15,16 @@
from compas_model.elements import Feature


class BlockGeometry(Mesh):
def centroid(self):
"""Compute the centroid of the block.

Returns
-------
:class:`compas.geometry.Point`

"""
x, y, z = centroid_points([self.vertex_coordinates(key) for key in self.vertices()])
return Point(x, y, z)
def invert(self):
self._yaxis = self._yaxis * -1
self._zaxis = self._zaxis * -1

def frames(self):
"""Compute the local frame of each face of the block.

Returns
-------
dict
A dictionary mapping face identifiers to face frames.
Frame.invert = invert

"""
return {face: self.frame(face) for face in self.faces()}

def frame(self, face):
class BlockGeometry(Mesh):
def face_frame(self, face):
"""Compute the frame of a specific face.

Parameters
Expand All @@ -52,11 +38,12 @@ def frame(self, face):

"""
xyz = self.face_coordinates(face)
o = self.face_center(face)
w = self.face_normal(face)
u = [xyz[1][i] - xyz[0][i] for i in range(3)] # align with longest edge instead?
v = cross_vectors(w, u)
return Frame(o, u, v)
normal = self.face_normal(face)
o, u, v = bestfit_frame_numpy(xyz)
frame = Frame(o, u, v)
if frame.zaxis.dot(normal) < 0:
frame.invert()
return frame

def top(self):
"""Identify the *top* face of the block.
Expand All @@ -72,6 +59,17 @@ def top(self):
normals = [self.face_normal(face) for face in faces]
return sorted(zip(faces, normals), key=lambda x: dot_vectors(x[1], z))[-1][0]

def centroid(self):
"""Compute the centroid of the block.

Returns
-------
:class:`compas.geometry.Point`

"""
x, y, z = centroid_points([self.vertex_coordinates(key) for key in self.vertices()])
return Point(x, y, z)

def center(self):
"""Compute the center of mass of the block.

Expand Down Expand Up @@ -138,53 +136,67 @@ class BlockElement(Element):

"""

elementgeometry: BlockGeometry
modelgeometry: BlockGeometry

@property
def __data__(self):
# type: () -> dict
data = super(BlockElement, self).__data__
def __data__(self) -> dict:
data = super().__data__
data["shape"] = self.shape
data["features"] = self.features
data["is_support"] = self.is_support
return data

def __init__(self, shape, features=None, is_support=False, frame=None, transformation=None, name=None):
# type: (Mesh | BlockGeometry, list[BlockFeature] | None, bool, compas.geometry.Frame | None, compas.geometry.Transformation | None, str | None) -> None
super().__init__(frame=frame, transformation=transformation, name=name)

super(BlockElement, self).__init__(frame=frame, transformation=transformation, name=name)
self.shape = shape if isinstance(shape, BlockGeometry) else shape.copy(cls=BlockGeometry)
self.features = features or [] # type: list[BlockFeature]
self.is_support = is_support

# don't like this
# but want to test the collider
@property
def face_polygons(self):
# type: () -> list[compas.geometry.Polygon]
return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore

# =============================================================================
# Implementations of abstract methods
# =============================================================================

def compute_geometry(self, include_features=False):
def compute_elementgeometry(self):
geometry = self.shape
if include_features:
if self.features:
for feature in self.features:
geometry = feature.apply(geometry)
geometry.transform(self.worldtransformation)
# apply features?
return geometry

def compute_modelgeometry(self):
if not self.model:
raise Exception

geometry = self.elementgeometry.transformed(self.modeltransformation)

# apply effect of interactions?
node = self.graphnode
nbrs = self.model.graph.neighbors_in(node)
for nbr in nbrs:
element = self.model.graph.node_element(nbr)
if element:
for interaction in self.model.graph.edge_interactions((nbr, node)):
# example interactions are
# cutters, boolean operations, slicers, ...
if hasattr(interaction, "apply"):
try:
interaction.apply(geometry)
except Exception:
pass

return geometry

def compute_aabb(self, inflate=0.0):
points = self.geometry.vertices_attributes("xyz") # type: ignore
points = self.modelgeometry.vertices_attributes("xyz") # type: ignore
box = Box.from_bounding_box(bounding_box(points))
box.xsize += inflate
box.ysize += inflate
box.zsize += inflate
return box

def compute_obb(self, inflate=0.0):
points = self.geometry.vertices_attributes("xyz") # type: ignore
points = self.modelgeometry.vertices_attributes("xyz") # type: ignore
box = Box.from_bounding_box(oriented_bounding_box(points))
box.xsize += inflate
box.ysize += inflate
Expand All @@ -195,7 +207,7 @@ def compute_collision_mesh(self):
# TODO: (TvM) make this a pluggable with default implementation in core and move import to top
from compas.geometry import convex_hull_numpy

points = self.geometry.vertices_attributes("xyz") # type: ignore
points = self.modelgeometry.vertices_attributes("xyz") # type: ignore
vertices, faces = convex_hull_numpy(points)
vertices = [points[index] for index in vertices] # type: ignore
return Mesh.from_vertices_and_faces(vertices, faces)
Expand Down
Loading
Loading