Skip to content

Read the Breps with their adjacencyยค

The page before draws the STEP files. This one asks them what is connected to what, which needs the third file: the JSON sidecar.

One element's joints highlighted in red across the ghosted building

The 17 contacts on inner_ribs_0_0 in red, the rest ghosted behind them. The sidecar is what turns "face 412" into "this rib meets that bed".

import json
import pathlib
from collections import defaultdict

from compas.colors import Color
from compas.tolerance import TOL
from compas_occt.brep import OCCBrep
from compas_viewer import Viewer

from compas_tf.viewer import zoom_to

data_dir = pathlib.Path(__file__).parent.parent / "data"
STEP_FILE = data_dir / "cantilevers_baked_model.stp"
CONTACTS_STEP_FILE = data_dir / "cantilevers_baked_contacts.stp"
CONTACTS_JSON_FILE = data_dir / "cantilevers_baked_contacts.json"

# The 0.001 default is 1 micron on a building: 2.93M triangles against 19.8k.
TOL.lineardeflection = 1.0

solids = OCCBrep.from_step(STEP_FILE).solids
faces = OCCBrep.from_step(CONTACTS_STEP_FILE).faces
records = json.loads(CONTACTS_JSON_FILE.read_text())["contacts"]

# STEP drops names but keeps ORDER, so record i describes face i. Equal counts is
# the only check there is that the two files came from the same run.
if len(records) != len(faces):
    raise SystemExit(f"{len(faces)} faces against {len(records)} records: the STEP and its sidecar are out of sync.")

print(f"{len(solids)} solids, {len(faces)} contact faces, {len(records)} adjacency records")

# A graph over element NAMES: the solids themselves stay anonymous. For geometry
# with a name on it, read the JSON model instead (example_model_19).
neighbours = defaultdict(set)
joints = defaultdict(list)  # (a, b) -> the face indices they share
for record in records:
    neighbours[record["a"]].add(record["b"])
    neighbours[record["b"]].add(record["a"])
    joints[record["a"], record["b"]].append(record["index"])

area = sum(record["area"] for record in records)
print(f"{len(neighbours)} elements over {len(joints)} pairs, {area / 1e6:.2f} m2 of interface")

# Several faces on one pair is one joint the boolean left in pieces, so group by
# pair before ranking or it sorts below a single big face.
print("\nbiggest joints:")
for (a, b), indices in sorted(joints.items(), key=lambda kv: -sum(records[i]["area"] for i in kv[1]))[:5]:
    print(f"  {sum(records[i]['area'] for i in indices) / 1e6:6.3f} m2  {a} - {b}  ({len(indices)} faces)")

by_type = defaultdict(float)
for record in records:
    by_type[tuple(sorted((record["a_type"], record["b_type"])))] += record["area"]
print("\nby type:")
for (a, b), value in sorted(by_type.items(), key=lambda kv: -kv[1]):
    print(f"  {value / 1e6:6.3f} m2  {a} - {b}")

# One element's joints out of the 733, drawn over a transparent model - every
# contact sits BETWEEN two solids.
focus = max(neighbours, key=lambda n: len(neighbours[n]))
selected = {record["index"] for record in records if focus in (record["a"], record["b"])}
print(f"\n{len(selected)} contacts on {focus}, the most connected element")

viewer = Viewer()

parts = viewer.scene.add_group("model")
for solid in solids:
    viewer.scene.add(solid, parent=parts, opacity=0.3)

group = viewer.scene.add_group("contacts")
highlighted = viewer.scene.add_group(f"contacts__{focus}")
for record, face in zip(records, faces):
    hit = record["index"] in selected
    viewer.scene.add(
        face.to_polygon(),
        # The name STEP could not carry, put back from the sidecar.
        name=f"contact_{record['index']}__{record['a']}__{record['b']}",
        parent=highlighted if hit else group,
        facecolor=Color(1, 0, 0) if hit else Color(0.6, 0.6, 0.6),
        linecolor=Color(1, 0, 0) if hit else Color(0.4, 0.4, 0.4),
        show_points=False,
    )

# The camera's far plane is 1000 mm, so without this the building starts clipped.
zoom_to(viewer, [solid.aabb for solid in solids])

viewer.show()
237 solids, 733 contact faces, 733 adjacency records
169 elements over 585 pairs, 20.92 m2 of interface

biggest joints:
   0.344 m2  inner_beams_0_1 - connector_wedge_3  (3 faces)
   0.344 m2  inner_beams_2_2 - connector_wedge_3  (3 faces)
   0.344 m2  inner_beams_0_0 - connector_wedge_0  (3 faces)
   0.344 m2  inner_beams_2_1 - connector_wedge_0  (3 faces)
   0.344 m2  inner_beams_0_2 - connector_wedge_5  (3 faces)

by type:
  12.818 m2  PlateElement - PlateElement
   4.459 m2  ConnectorWedgeElement - PlateElement
   1.106 m2  ColumnElement - PlateElement
   1.101 m2  ConnectorElement - PlateElement
   0.895 m2  ColumnElement - ConnectorElement
   0.541 m2  OuterRibConnectorElement - PlateElement

17 contacts on inner_ribs_0_0, the most connected element

A record per face, in the same order the faces were written:

{"index": 0, "a": "beds_0_0_0", "b": "wedges_inner_beams_0_0",
 "a_type": "PlateElement", "b_type": "PlateElement",
 "a_guid": "c46c8acc-...", "b_guid": "3098146e-...",
 "area": 39851.67}

Three things worth knowing:

Index is the only join. Equal counts is the one check that the two files came from the same run - pair a STEP with the wrong sidecar and every name lands on the wrong face, silently.

The names are element names. The solids stay anonymous; nothing here says which of the 237 is beds_0_0_0. For that, read the JSON model.

A pair is not a face. 585 pairs carry 733 faces, so group by (a, b) before ranking or one joint split in three sorts below a single big one.