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
-
How can I structure my code so that I only import from
compas
core, and havecompas_libigl
orcompas_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 avoidPluginNotInstalledError
. -
What’s the correct usage of
Brep.from_loft()
fromcompas.geometry.brep
?- What input types does it expect?
- Can I pass
Polyline
objects directly? - 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