PluginNotInstalledError using COMPAS with plugins (compas_libigl, compas_occ)

I’m currently integrating COMPAS into CityEnergyAnalyst, where we generate 3D building geometry from 2D outlines. I chose COMPAS for its active development and modular structure. However, I’ve been running into confusion and runtime errors when working with its plugin system, especially with compas_libigl and compas_occ.

My core questions

  1. How can I structure my code so that I only import from compas core, and have compas_libigl or compas_occ automatically resolve at runtime?
    Right now, I’m forced to import plugin methods explicitly (e.g., from compas_libigl.intersections import intersection_ray_mesh) to avoid PluginNotInstalledError.

  2. What’s the correct usage of Brep.from_loft() from compas.geometry.brep?

    1. What input types does it expect?
    2. Can I pass Polyline objects directly?
    3. Or must I convert them to OCCCurve, and if so, how?

Problem 1: Ray-mesh intersection for terrain elevation

I want to cast a vertical ray (point + vector) from a building footprint to a terrain mesh and retrieve the intersection point. Code snippet:

from compas.geometry import intersection_ray_mesh
hits = intersection_ray_mesh(ray, surface.to_vertices_and_faces())

This raises PluginNotInstalledError. Even though compas_libigl is installed, this only works if I explicitly import:

from compas_libigl.intersections import intersection_ray_mesh

This seems to bypass COMPAS’s plugin system. How should I structure my environment/code so that compas core handles this dynamically?

Complete code snippet:
from compas.datastructures import Mesh
from compas.geometry import Point, Vector, intersection_ray_mesh
# from compas_libigl.intersections import intersection_ray_mesh


def calc_intersection(
    surface: Mesh, edges_coords: Point, edges_dir: Vector
) -> Tuple[int | None, Point | None]:
    """This script calculates the intersection of a ray from a particular point to the terrain.

    :param surface: the terrain mesh to be intersected.
    :type surface: Mesh
    :param edges_coords: the coordinates of the point from which the ray is cast.
    :type edges_coords: Point
    :param edges_dir: the direction of the ray, which is usually a vector pointing upwards.
    :type edges_dir: Vector
    :return: a tuple containing the index of the face that was hit and the intersection point.
        if no intersection was found, it returns (None, None).
    :rtype: Tuple[int | None, Point | None]
    """
    ray = (edges_coords, edges_dir)
    hits: list[tuple[int, float, float, float]] = intersection_ray_mesh(ray, surface.to_vertices_and_faces())
    idx_face, u, v, t = hits[0] if hits else (None, None, None, None)
    # u, v are the barycentric coordinates of the intersection point on the face.
    if idx_face is not None:
        face_points = surface.face_points(idx_face)
        w = 1 - u - v
        p0 = face_points[0]
        p1 = face_points[1]
        p2 = face_points[2]
        inter_pt = Point(
            p0.x * w + p1.x * v + p2.x * u,
            p0.y * w + p1.y * v + p2.y * u,
            p0.z * w + p1.z * v + p2.z * u
        )
        return idx_face, inter_pt
    else:
        # No intersection found
        return None, None

Problem 2: Lofting floors with Brep.from_loft()

I attempt to extrude a building footprint using Brep.from_loft():

walls = Brep.from_loft(floor_edges)

Each floor_edges item is a Polyline created by vertically translating the footprint. But this also raises PluginNotInstalledError.

To resolve it, I tried:

from compas_occ.brep import OCCBrep
from compas_occ.geometry import OCCCurve

walls = OCCBrep.from_loft([OCCCurve(edge) for edge in floor_edges])

But this time I get:

TypeError: Wrong number or type of arguments for overloaded function 'new_BRepBuilderAPI_MakeEdge'.

So my questions are:

  • Is Polyline the correct type here, or do I need to manually convert to a list of OCC-compatible curves?
  • Is there a helper method to wrap COMPAS curves into valid OCCCurve instances?
  • What’s the expected interface for lofting with compas_occ?
Complete code snippet
from compas.geometry import Polygon, PolyLine, Vector, Translation
from compas.geometry import Brep


def calc_solid(face_footprint: Polygon, 
               range_floors: range, 
               floor_to_floor_height: float,
               ) -> list[Brep]:
    """
    extrudes the footprint surface into a 3D solid.

    :param face_footprint: footprint of the building. 
    :type face_footprint: Polygon
    :param range_floors: 
        range of floors for the building. 
        For example, a building of 3 floors will have `range(4) = [0, 1, 2, 3]`, 
        because it has 4 floors (1 ground floor + 2 internal ceilings + 1 roof).
    :type range_floors: range
    :param floor_to_floor_height: the height of each level of the building.
        For example, if the building has 3 floors and a height of 9m, then `floor_to_floor_height = 9 / 3 = 3`.
    :type floor_to_floor_height: float
    :return: a solid representing the building, made from footprint + vertical external walls + roof.
    :rtype: list[Brep]
    """
    footprint_edge = Polyline(list(face_footprint.vertices) + [face_footprint.vertices[0]])
    floor_edges = [footprint_edge]
    for i_floor in range_floors:
        if i_floor == 0:
            pass
        floor_edges.append(
            footprint_edge.transformed(
                Translation.from_vector(Vector(0, 0, i_floor * floor_to_floor_height))
            )
        )

    walls = Brep.from_loft(floor_edges)
    roof_polygon = face_footprint.transformed(
        Translation.from_vector(Vector(0, 0, range_floors[-1] * floor_to_floor_height))
    )
    roof = Brep.from_polygons([roof_polygon])
    floor = Brep.from_polygons([face_footprint])

    return [floor, walls, roof]

General Information

My setup:

  • Python 3.12 (micromamba)
  • compas == 2.13.0
  • compas_libigl == 0.7.4
  • compas_occ == 1.3.0

Generally, I would like to know how to write code that only depends on the core API but uses plugin functionality when available. Any clarification or guidance is greatly appreciated!

Best regards
Yiqiao

hi yiqiao,

in principle it should indeed be the case that the plugins from packages like compas_libigl are automatically detected by the pluggables in compas. the only requirement is that compas and the plugin package are installed in the same environment.

i have to check, but it is certainly possible that compas_libigl doesn’t register its intersection functions as plugins for the corresponding core pluggables yet.

this is probably because, until recently, we didn’t have a reliable build system for compas_libigl and it was therefore not considered an official core framework package.

i will have a look and fix asap…

regarding the implementation of lofting in compas_occ. this is also a recent addition. i will have a look and try to fix that as well…

best,
tom

1 Like

Thank you, @tomvanmele , this information is very helpful! I look forward to the updates! :slight_smile:

compas_libigl is updated. it should now include some of the missing requirements, and properly register the plugins. @petrasvestartas could you check to make sure :slight_smile:

I am running this code with:

conda create -n compas_env -c conda-forge compas

conda activate compas_env

pip install compas_libigl compas_viewer

python ray_mesh.py

The code of the ray_mesh.py is below and by default I have Python 3.13.5

import compas
import numpy as np
from compas.colors import Color
from compas.datastructures import Mesh
from compas.geometry import Line
from compas.geometry import Point
from compas_viewer import Viewer

# from compas_libigl.intersections import intersection_rays_mesh
from compas.geometry import intersection_ray_mesh

# ==============================================================================
# Input geometry
# ==============================================================================

mesh = Mesh.from_obj(compas.get("tubemesh.obj"))

trimesh = mesh.copy()
trimesh.quads_to_triangles()

# ==============================================================================
# Rays
# ==============================================================================

base = Point(*mesh.centroid())
base.z = 0

theta = np.linspace(0, np.pi, 20, endpoint=False)
phi = np.linspace(0, 2 * np.pi, 20, endpoint=False)
theta, phi = np.meshgrid(theta, phi)
theta = theta.ravel()
phi = phi.ravel()
r = 1.0
x = r * np.sin(theta) * np.cos(phi) + base.x
y = r * np.sin(theta) * np.sin(phi) + base.y
z = r * np.cos(theta)

xyz = np.vstack((x, y, z)).T
mask = xyz[:, 2] > 0
hemi = xyz[mask]

rays = []
for x, y, z in hemi:
    point = Point(x, y, z)
    vector = point - base
    vector.unitize()
    rays.append((base, vector))

# ==============================================================================
# Intersections
# ==============================================================================

index_face = {index: face for index, face in enumerate(mesh.faces())}

hits_per_ray = []
for ray in rays:
    hits = intersection_ray_mesh(ray, mesh.to_vertices_and_faces())
    hits_per_ray.append(hits)




intersections = []
for ray, hits in zip(rays, hits_per_ray):
    if hits:
        base, vector = ray
        index = hits[0][0]
        distance = hits[0][3]
        face = index_face[index]
        point = base + vector * distance
        intersections.append(point)

# ==============================================================================
# Visualisation
# ==============================================================================

viewer = Viewer(width=1600, height=900)

viewer.scene.add(mesh, opacity=0.7, show_points=False)

for intersection in intersections:
    viewer.scene.add(Line(base, intersection), linecolor=Color.blue(), linewidth=3)

viewer.show()
1 Like

And I am waiting for your answer here:

Since I made this pull request for you that has to be tested, if this is what you want:

cool that it works properly now, but perhaps we should make a wrapper that simplifies the use of the function :slight_smile:

I will add another return type, a point, which will be the most use case.

Updated in the new pull request:

# ==============================================================================
# Intersections
# ==============================================================================

hits_per_rays = intersection_rays_mesh(rays, trimesh.to_vertices_and_faces())

intersection_points = []
for hits_per_ray in hits_per_rays:
    if hits_per_ray:
        for hit in hits_per_ray:
            pt, idx, u, v, w = hit
            intersection_points.append(pt)

1 Like

Thanks @tomvanmele, @petrasvestartas ! The compas_libigl part of my question is much clearer now.

Regarding compas_occ: my ultimate goal is to create 3D building geometry with LOD3 accuracy as a Brep in python. I currently have the footprint polygon, number of floors, floor height, window-to-wall ratio and I want to create a flat-roof-building Brep with windows and walls for each of the floor. For that I rely on compas_occ.

So far, I have tried from_loft, from_extrusion and from_polygon, but I couldn’t get a useful Brep, and the examples out there are quite minimal (and I am not used to the plugin/pluggable system, it’s hard to debug inside IDE because it just traces back to a NotImplementedError…)

Do you have any suggestions on how to build such a 3D building with Python and compas? Which of the methods are more flexible / reliable? Thanks again!