Skip to content

compas_tf.contacts ¤

Contact detection on Brep faces instead of mesh faces.

A contact is two coplanar, oppositely oriented, overlapping polygons. Where those polygons come from decides the answer:

  • mesh_mesh_contacts uses element.modelgeometry - triangles, after a boolean. One physical interface comes back as several contact polygons (the column/outer-rib joint splits into 8) from an O(faces_a x faces_b) search over the soup: 626 x 246 faces, ~13 s for that one pair. It also drops area - on one of the 8 identical column/rib joints it returns 50344 mm2 instead of 62632, invisibly, because the interface is already in pieces.
  • brep_brep_contacts uses Brep faces. :func:compas_tf.brep.mesh_to_brep merges those triangles back into the modelled faces and a Brep face carries its hole loops, so the same joint is 1 contact of the full area in ~0.2 s.

Breps cannot simply be put on the elements - Element.compute_aabb and the BVH are Mesh-only - so :class:BrepContacts keeps the Mesh model, lets the BVH prune on the mesh AABBs, and converts only the pairs that survive.

Local, not upstream. :func:brep_brep_contacts is compas_model's function with the failures contained: upstream lets one bad face pair kill the whole element pair, and 23 of 2444 pairs here die inside brepface_brepface_overlap_holes (shapely: GeometryCollection has no attribute 'interiors', unable to assign free hole to a shell) AFTER the contact polygon computed fine. Each face pair is guarded on its own, so that costs the holes, not the contact. The geometry itself is still upstream's.

No tessellation. Brep.overlap would prefilter the face pairs for us, but it works on OCC's tessellation and triangulates each Brep on first use - at TOL.lineardeflection (0.001 mm, a 1 micron chord tolerance on a building) that meshing alone was 200 s of a 205 s search. It is not needed: a contact needs two faces with exactly opposite normals whose boundary AABBs meet, and both are exact tests on the face boundaries. :func:prepare_faces takes those once per Brep and :func:brep_brep_contacts filters on them, for identical results (585 pairs, 2.0921e+07 mm2) 3.8x faster than overlap() even with its tessellation already cached.

Do not narrow the prefilter to face.is_plane: these Breps are built from mesh polygons, so a flat quad comes back as a bilinear surface and the planar test drops 529 of the 585 pairs.

Classes¤

BrepContacts ¤

BrepContacts(holes: bool = True, skip: Callable | None = None, strict: bool = False, **brepkwargs)

Element-pair contact detection on Breps, with a Brep cache.

Callable with the signature the model's contact search expects ((a, b, tolerance=, minimum_area=, contacttype=) -> list[Contact]), so it goes straight into contactmethod=::

method = BrepContacts()
model.compute_contacts(minimum_area=1.0, contactmethod=method)
print(len(method.breps), "breps,", len(method.errors), "face pairs failed")

or, the same thing in one call, :meth:compas_tf.model.TFModel.compute_contacts_brep.

Each element is converted to a Brep at most once - conversion is the expensive half (~55 ms per element on the cantilevers model, vs ~85 ms per element PAIR for the detection), and every element takes part in several pairs.

Parameters:

Name Type Description Default
holes bool

Compute the hole loops of each contact.

True
skip callable

(a, b) -> bool; a pair it accepts is dropped before either element is even converted to a Brep, so skipping is free. :func:involving and :func:between build the usual ones.

None
strict bool

Let a failing face pair raise instead of being recorded in :attr:errors.

False
**brepkwargs

Forwarded to element.get_brep() - e.g. merge_coplanar=False, though merging is exactly what makes this worth doing.

{}

Attributes:

Name Type Description
breps dict[int, :class:`compas_occt.brep.OCCBrep`]

The cache, keyed by id(element).

errors list[tuple[str, str, str, Exception]]

(element_a, element_b, stage, exception) per failed face pair.

skipped int

How many pairs skip rejected.

Methods:¤

brep ¤
brep(element)

The element's Brep, converting (and caching) on first use.

Returns None for an element that has no geometry to convert.

faces ¤
faces(element, minimum_area: float) -> list

:func:prepare_faces for an element's Brep, cached.

Functions:¤

between ¤

between(*types) -> Callable

A skip predicate that rejects a pair only if BOTH elements are one of types.

The narrower companion to :func:involving: it removes contacts within a set of elements while keeping the contacts each of them makes with the rest of the model.

Parameters:

Name Type Description Default
*types type

Element classes, matched with isinstance.

()

Returns:

Type Description
callable

(a, b) -> bool, suitable as :class:BrepContacts's skip.

brep_brep_contacts ¤

brep_brep_contacts(a, b, tolerance: float = 1e-06, minimum_area: float = 0.1, contacttype: type[Contact] = Contact, holes: bool = True, errors: list | None = None, faces_a: list | None = None, faces_b: list | None = None)

Face-face contacts between two Breps - one contact polygon per interface.

Parameters:

Name Type Description Default
a :class:`compas_occt.brep.OCCBrep`

The two solids.

required
b :class:`compas_occt.brep.OCCBrep`

The two solids.

required
tolerance float

Distance tolerance: maximum deviation from the perfectly flat interface plane.

1e-06
minimum_area float

Contacts (and holes) smaller than this are dropped.

0.1
contacttype type[:class:`compas_model.interactions.Contact`]

The contact class to instantiate.

Contact
holes bool

Compute the hole loops of each contact. Turn off to skip the one part of the computation that is allowed to fail (see the module docstring). Ignored on a compas_model too old to return the transformations.

True
errors list

Appended to with (stage, exception) for every face pair that raised, instead of the exception propagating. stage is "overlap" or "holes".

None
faces_a list

:func:prepare_faces output for a and b, to reuse across pairs.

None
faces_b list

:func:prepare_faces output for a and b, to reuse across pairs.

None

Returns:

Type Description
list[:class:`compas_model.interactions.Contact`]

contact_holes ¤

contact_holes(contact: Contact) -> list

The hole loops of a contact, or an empty list.

Contact stores them (and serializes them, and builds them into contact.brep) but exposes no accessor, unlike contact.polygon.

Parameters:

Name Type Description Default
contact :class:`compas_model.interactions.Contact`
required

Returns:

Type Description
list[:class:`compas.geometry.Polygon`]

involving ¤

involving(*types) -> Callable

A skip predicate that rejects a pair if EITHER element is one of types.

For dropping whole classes of element from a contact search - typically fasteners, whose contacts are a shaft sitting in its own hole. On the cantilevers model the dowels and connector cylinders account for 2072 of the 2805 contacts (each faceted shaft touches its host once per facet), and none of them touch each other, so they are pure noise over the 733 real interfaces::

model.compute_contacts_brep(skip=involving(DowelCylinderElement, ConnectorCylinderElement))

Parameters:

Name Type Description Default
*types type

Element classes, matched with isinstance.

()

Returns:

Type Description
callable

(a, b) -> bool, suitable as :class:BrepContacts's skip.

prepare_faces ¤

prepare_faces(brep, minimum_area: float = 0.1) -> list

(face, points, normal, aabb) per face of a Brep, for the pair loop.

Do this once per Brep and reuse it across every pair the Brep takes part in; :class:BrepContacts caches it. Recomputing per pair is what makes the naive loop slow, not the intersections.

Parameters:

Name Type Description Default
brep :class:`compas_occt.brep.OCCBrep`
required
minimum_area float

Faces smaller than this cannot carry a contact and are dropped here.

0.1

Returns:

Type Description
list[tuple]

aabb is (xmin, ymin, zmin, xmax, ymax, zmax) of the face boundary.