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!

if you give me some sample input geometry and describe what you want as a result, i can write you an example script…

Hi! I am able to create a 3-floor building with windows on each floor of its facade. I used from_polygon to create this brep, and later I need to send this brep to daysim to do daylight simulation, therefore I need window geometries…

The input of the whole file is:

  1. building footprint as 2d polyline/polygon/points
  2. its window-to-wall ratio
  3. numbers of floors and floor height
  4. ground as a mesh, for calculating the building’s elevation.

There are in total four steps:

  1. identify the building’s elevation by intersecting its footprint’s centroid to the ground mesh.
  2. move the footprint to each of the floor.
  3. create walls and ceilings of this floor. If there’s window (wwr>0), then also create a window on each wall of this floor. Then merge all polygons into one brep, containing all the subsurface information.
  4. merge all floor-breps into a building-brep.

In this process, I noticed some difficulties, and wonder if there’s some possible way to make it simpler and more elegant:

  1. The type hint in intersection_ray_mesh from compas.geometry is NoReturn (it works now though), which bothers with the exact content of its return (though it’s commented in the docstring);
  2. scaling of polygon is around the world origin, which is unclear in the documentation.
  3. polygons do not have a “edges” attribute which returns a polyline, although their constructions are very similar.
  4. difficulties of extruding from the floor. I have to manually assemble the brep using walls, windows, floors and ceilings. It would be nice if I could simply extrude from the polyline.

I have my test code and the result screenshot pasted below for your reference. Please let me know if you have any advice! Thanks in advance!

from compas.geometry import Point, Line, Vector, Polygon, intersection_ray_mesh
from compas.datastructures import Mesh
from compas_occ.brep import OCCBrep
from compas_viewer import Viewer
import math

UP = Vector(0, 0, 1)

def extrude_line_to_polygon(line: Line, height: float, direction: Vector = UP) -> Polygon:
    """Make a rectangular wall polygon by extruding a line along a direction."""
    v = direction.unitized().scaled(height)
    return Polygon([line.start, line.end, line.end.translated(v), line.start.translated(v)])

def open_window_in_wall(wall: Polygon, wwr: float) -> tuple[list[Polygon], Polygon]:
    """open a window in the center of the polygon, and cut the the original polygon with a hole.

    :param polygon: input polygon (typically a wall).
    :type polygon: Polygon
    :param wwr: window-to-wall ratio (0.0 - 1.0)
    :type wwr: float
    :return: original polygon with a hole (cut into four trapezoids through its corners), and the window polygon
    :rtype: tuple[list[Polygon], Polygon]
    """
    polygon_win: Polygon = wall.scaled(math.sqrt(wwr)) # scaled relative to world origin
    polygon_win.translate(Vector.from_start_end(polygon_win.centroid, wall.centroid))
    polygons_wall: list[Polygon] = []
    for i in range(len(wall.lines)):
        line_wall = wall.lines[i]
        line_win = polygon_win.lines[i]
        wall_trapezoid = Polygon([line_wall.start, line_wall.end, line_win.end, line_win.start])
        polygons_wall.append(wall_trapezoid)
    return polygons_wall, polygon_win
    
def ground_z_under(mesh: Mesh, origin_point, direction: Vector = UP) -> float:
    """Cast a ray and return the Z at the first hit via barycentric interpolation."""
    hit = intersection_ray_mesh((origin_point, direction), mesh.to_vertices_and_faces())
    if not hit:
        raise ValueError("No ground hit under the footprint.")
    fkey, u, v, _ = hit[0]
    a, b, c = mesh.face_points(fkey)  # triangle
    w = 1 - u - v
    return w * a.z + u * b.z + v * c.z

def make_floor_brep(footprint_at_z: Polygon, floor_height: float, wwr: float) -> OCCBrep:
    """Build one floor solid from: slab + walls + ceiling."""
    slab = footprint_at_z
    walls = [extrude_line_to_polygon(edge, floor_height, UP) for edge in slab.lines]
    if wwr > 0:
        walls_with_holes: list[Polygon] = []
        windows: list[Polygon] = []
        for i, wall in enumerate(walls):
            wall_with_hole, window = open_window_in_wall(wall, wwr)
            walls_with_holes.extend(wall_with_hole)
            windows.append(window)
        walls = walls_with_holes
    else:
        windows = []
    ceiling = slab.translated(Vector(0, 0, floor_height))
    return OCCBrep.from_polygons([slab, *walls, *windows, ceiling])

# --- scene setup --------------------------------------------------------------
viewer = Viewer()

# square footprint and a simple 'ground' mesh at z=10
footprint = Polygon([Point(0, 0, 0), Point(1, 0, 0), Point(1, 1, 0), Point(0, 1, 0)])
ground = Mesh.from_points([Point(0, 0, 10), Point(2, 0, 10), Point(2, 2, 10), Point(0, 2, 10)])
wwr = 0.4
floor_height = 3.0
floor_range = range(2, 5)

viewer.scene.add(footprint)
viewer.scene.add(ground)

# --- find elevation and place floors -----------------------------------------
elevation = ground_z_under(ground, footprint.centroid)
base_footprint = footprint.translated(Vector(0, 0, elevation))

breps: list[OCCBrep] = []
for floor in floor_range:  # floors 2, 3, 4 (building floats)
    z = floor * floor_height
    brep = make_floor_brep(base_footprint.translated(Vector(0, 0, z)), floor_height, wwr)
    breps.append(brep)

final_brep = breps[0].boolean_union(*breps[1:])
viewer.scene.add(final_brep)
viewer.show()

I also encountered some NotImplementedError in compas_occ:

  1. OCCBrep.compute_aabb not implemented. This is relatively easy to solve by writing something myself.
  2. OCCBrep.contains not implemented for compas.geometry.Point.
  3. when saving my my list containing OCCBrepEdge to hard drive using pickle, pickle asks for its surface property, and it tries to convert it to standard surface geometries. However, my shape is not recognized as a plane, therefore it raises NotImplementedError. This prevents the data to be saved to hard drive… I tried to convert all of them to Polygons before saving.

Do you think there are walkarounds to these questions? Thanks!