Skip to content

Geometry Builders

Geometry utility functions for EnergyPlus surface manipulation. For creating building zones and surfaces, see Zoning.

Quick Start

from idfkit import new_document
from idfkit.geometry_builders import add_shading_block

doc = new_document()
add_shading_block(doc, "Neighbour", [(30, 0), (50, 0), (50, 20), (30, 20)], height=25)

print(len(doc["Shading:Site:Detailed"]))  # 5

Shading Blocks

add_shading_block creates Shading:Site:Detailed surfaces -- opaque boxes that cast shadows but have no thermal zones.

from idfkit import new_document
from idfkit.geometry_builders import add_shading_block

doc = new_document()

# Neighbouring building
add_shading_block(doc, "Neighbour", [(30, 0), (50, 0), (50, 20), (30, 20)], height=25)

# Elevated canopy
add_shading_block(doc, "Canopy", [(0, -3), (10, -3), (10, 0), (0, 0)], height=0.2, base_z=3)

Each call creates one wall surface per footprint edge plus a horizontal top cap.

GlobalGeometryRules Convention

All builder functions read the document's GlobalGeometryRules to determine the vertex ordering convention:

  • starting_vertex_position -- which corner is listed first for walls (UpperLeftCorner, LowerLeftCorner, etc.)
  • vertex_entry_direction -- winding direction (Counterclockwise or Clockwise)

When no GlobalGeometryRules object exists, the EnergyPlus default of UpperLeftCorner / Counterclockwise is assumed.

This means you can safely add geometry to an existing model that uses a non-default convention without having to rewrite all existing surfaces:

from idfkit import load_idf, create_building

# Model uses Clockwise vertex convention
model = load_idf("existing_building.idf")

# New surfaces will automatically use Clockwise ordering
# to match the model's GlobalGeometryRules
create_building(model, "Addition", [(20, 0), (30, 0), (30, 10), (20, 10)], floor_to_floor=3)

Wall Vertex Order by Convention

For a wall between footprint vertices p1 and p2 (height z_bot to z_top), viewed from outside:

Starting Position Counterclockwise Clockwise
UpperLeftCorner UL LL LR UR UL UR LR LL
LowerLeftCorner LL LR UR UL LL UL UR LR
LowerRightCorner LR UR UL LL LR LL UL UR
UpperRightCorner UR UL LL LR UR LR LL UL

Where UL = (p1, z_top), LL = (p1, z_bot), LR = (p2, z_bot), UR = (p2, z_top).

Horizontal Surfaces

For floors and ceilings, the winding direction is adapted so that EnergyPlus computes the correct outward normal regardless of convention:

  • Floor: outward normal points down (toward ground)
  • Ceiling / Roof: outward normal points up (toward sky)

Utility Functions

set_default_constructions

Assigns a placeholder construction name to any surface that lacks one:

from idfkit.geometry_builders import set_default_constructions

count = set_default_constructions(doc, "Generic Wall")
print(f"Updated {count} surfaces")

bounding_box

Returns the 2D axis-aligned bounding box of all BuildingSurface:Detailed objects:

from idfkit.geometry_builders import bounding_box

bbox = bounding_box(doc)
if bbox:
    (min_x, min_y), (max_x, max_y) = bbox
    print(f"Footprint spans {max_x - min_x:.1f} x {max_y - min_y:.1f} m")

scale_building

Scales all surface vertices around an anchor point:

from idfkit.geometry_builders import scale_building
from idfkit.geometry import Vector3D

# Double the building in all directions
scale_building(doc, 2.0)

# Stretch only the X axis
scale_building(doc, (1.5, 1.0, 1.0))

# Scale around the building centroid
scale_building(doc, 0.5, anchor=Vector3D(15, 10, 0))

API Reference

Geometry utility functions for EnergyPlus surface manipulation.

Provides shading block creation, default construction assignment, bounding box queries, building scaling, and GlobalGeometryRules vertex-ordering helpers.

For building zone and surface creation, see zoning which provides create_building and ZonedBlock.

add_shading_block(doc, name, footprint, height, base_z=0.0)

Create Shading:Site:Detailed surfaces from a 2D footprint.

Creates one shading surface per footprint edge (walls) plus a horizontal top cap. No zones or thermal surfaces are created.

Parameters:

Name Type Description Default
doc IDFDocument

The document to add objects to.

required
name str

Base name for shading surfaces.

required
footprint Sequence[tuple[float, float]]

2D footprint as (x, y) tuples (counter-clockwise).

required
height float

Height of the shading block in metres.

required
base_z float

Z-coordinate of the block base (default 0.0). Use this to create elevated shading surfaces such as canopies.

0.0

Returns:

Type Description
list[IDFObject]

List of created Shading:Site:Detailed objects.

Raises:

Type Description
ValueError

If footprint has fewer than 3 vertices or height <= 0.

Source code in src/idfkit/geometry_builders.py
def add_shading_block(
    doc: IDFDocument,
    name: str,
    footprint: Sequence[tuple[float, float]],
    height: float,
    base_z: float = 0.0,
) -> list[IDFObject]:
    """Create ``Shading:Site:Detailed`` surfaces from a 2D footprint.

    Creates one shading surface per footprint edge (walls) plus a
    horizontal top cap.  No zones or thermal surfaces are created.

    Args:
        doc: The document to add objects to.
        name: Base name for shading surfaces.
        footprint: 2D footprint as ``(x, y)`` tuples (counter-clockwise).
        height: Height of the shading block in metres.
        base_z: Z-coordinate of the block base (default ``0.0``).
            Use this to create elevated shading surfaces such as canopies.

    Returns:
        List of created ``Shading:Site:Detailed`` objects.

    Raises:
        ValueError: If footprint has fewer than 3 vertices or height <= 0.
    """
    fp = list(footprint)
    if len(fp) < 3:
        msg = f"Footprint must have at least 3 vertices, got {len(fp)}"
        raise ValueError(msg)
    if height <= 0:
        msg = f"Height must be positive, got {height}"
        raise ValueError(msg)

    svp, clockwise = get_geometry_convention(doc)
    wall_order = WALL_ORDER.get((svp, clockwise), (0, 1, 2, 3))

    z_bot = base_z
    z_top = base_z + height
    created: list[IDFObject] = []
    n = len(fp)

    # Walls
    for j in range(n):
        p1 = fp[j]
        p2 = fp[(j + 1) % n]
        wall_name = f"{name} Wall {j + 1}"
        corners = [
            Vector3D(p1[0], p1[1], z_top),  # UL
            Vector3D(p1[0], p1[1], z_bot),  # LL
            Vector3D(p2[0], p2[1], z_bot),  # LR
            Vector3D(p2[0], p2[1], z_top),  # UR
        ]
        poly = Polygon3D([corners[k] for k in wall_order])
        obj = doc.add("Shading:Site:Detailed", wall_name, validate=False)
        set_surface_coords(obj, poly)
        created.append(obj)

    # Top cap — horizontal surface with normal pointing up
    cap_name = f"{name} Top"
    cap = doc.add("Shading:Site:Detailed", cap_name, validate=False)
    set_surface_coords(cap, horizontal_poly(fp, z_top, reverse=clockwise))
    created.append(cap)

    return created

bounding_box(doc)

Return the 2D axis-aligned bounding box of all building surfaces.

Scans all BuildingSurface:Detailed vertices and returns the bounding envelope projected onto the XY plane.

Note

Only BuildingSurface:Detailed objects are considered. Fenestration and shading surfaces are excluded because they are either coplanar with (windows) or outside (shading) the thermal envelope.

Returns:

Type Description
tuple[tuple[float, float], tuple[float, float]] | None

((min_x, min_y), (max_x, max_y)) or None if no

tuple[tuple[float, float], tuple[float, float]] | None

surfaces with valid coordinates exist.

Source code in src/idfkit/geometry_builders.py
def bounding_box(doc: IDFDocument) -> tuple[tuple[float, float], tuple[float, float]] | None:
    """Return the 2D axis-aligned bounding box of all building surfaces.

    Scans all ``BuildingSurface:Detailed`` vertices and returns the
    bounding envelope projected onto the XY plane.

    !!! note
        Only ``BuildingSurface:Detailed`` objects are considered.
        Fenestration and shading surfaces are excluded because they
        are either coplanar with (windows) or outside (shading) the
        thermal envelope.

    Returns:
        ``((min_x, min_y), (max_x, max_y))`` or ``None`` if no
        surfaces with valid coordinates exist.
    """
    min_x = float("inf")
    min_y = float("inf")
    max_x = float("-inf")
    max_y = float("-inf")
    found = False

    for srf in doc["BuildingSurface:Detailed"]:
        coords = get_surface_coords(srf)
        if coords is None:
            continue
        for v in coords.vertices:
            min_x = min(min_x, v.x)
            min_y = min(min_y, v.y)
            max_x = max(max_x, v.x)
            max_y = max(max_y, v.y)
            found = True

    if not found:
        return None
    return ((min_x, min_y), (max_x, max_y))

get_geometry_convention(doc)

Read the vertex ordering convention from GlobalGeometryRules.

Returns:

Type Description
str

(starting_vertex_position, clockwise) where clockwise is

bool

True when vertex_entry_direction is "Clockwise".

tuple[str, bool]

Defaults to ("UpperLeftCorner", False) if no rules exist.

Source code in src/idfkit/geometry_builders.py
def get_geometry_convention(doc: IDFDocument) -> tuple[str, bool]:
    """Read the vertex ordering convention from ``GlobalGeometryRules``.

    Returns:
        ``(starting_vertex_position, clockwise)`` where *clockwise* is
        ``True`` when ``vertex_entry_direction`` is ``"Clockwise"``.
        Defaults to ``("UpperLeftCorner", False)`` if no rules exist.
    """
    geo_rules = doc["GlobalGeometryRules"]
    if not geo_rules:
        return ("UpperLeftCorner", False)
    rules = geo_rules.first()
    if rules is None:
        return ("UpperLeftCorner", False)
    svp = getattr(rules, "starting_vertex_position", None) or "UpperLeftCorner"
    ved = getattr(rules, "vertex_entry_direction", None) or "Counterclockwise"
    return (str(svp), str(ved).lower() == "clockwise")

horizontal_poly(footprint, z, *, reverse)

Build a horizontal polygon at height z.

When reverse is True the footprint is reversed, flipping the polygon normal. Used to produce floor and ceiling polygons in the correct winding for the active GlobalGeometryRules convention.

Source code in src/idfkit/geometry_builders.py
def horizontal_poly(footprint: list[tuple[float, float]], z: float, *, reverse: bool) -> Polygon3D:
    """Build a horizontal polygon at height *z*.

    When *reverse* is ``True`` the footprint is reversed, flipping the
    polygon normal.  Used to produce floor and ceiling polygons in the
    correct winding for the active ``GlobalGeometryRules`` convention.
    """
    pts = reversed(footprint) if reverse else footprint
    return Polygon3D([Vector3D(p[0], p[1], z) for p in pts])

scale_building(doc, factor, anchor=None)

Scale all surface vertices around an anchor point.

Parameters:

Name Type Description Default
doc IDFDocument

The document to modify in-place.

required
factor float | tuple[float, float, float]

Scale factor. A single float applies uniform scaling; a (fx, fy, fz) tuple scales each axis independently (e.g. (2.0, 1.0, 1.0) doubles X only).

required
anchor Vector3D | None

Point to scale around. If None, the origin (0, 0, 0) is used.

None
Source code in src/idfkit/geometry_builders.py
def scale_building(
    doc: IDFDocument,
    factor: float | tuple[float, float, float],
    anchor: Vector3D | None = None,
) -> None:
    """Scale all surface vertices around an anchor point.

    Args:
        doc: The document to modify in-place.
        factor: Scale factor.  A single ``float`` applies uniform scaling;
            a ``(fx, fy, fz)`` tuple scales each axis independently
            (e.g. ``(2.0, 1.0, 1.0)`` doubles X only).
        anchor: Point to scale around.  If ``None``, the origin
            ``(0, 0, 0)`` is used.
    """
    if isinstance(factor, tuple):
        fx, fy, fz = factor
    else:
        fx = fy = fz = factor

    ax, ay, az = (anchor.x, anchor.y, anchor.z) if anchor else (0.0, 0.0, 0.0)

    for stype in VERTEX_SURFACE_TYPES:
        for srf in doc[stype]:
            coords = get_surface_coords(srf)
            if coords is None:
                continue
            new_vertices = [
                Vector3D(
                    ax + (v.x - ax) * fx,
                    ay + (v.y - ay) * fy,
                    az + (v.z - az) * fz,
                )
                for v in coords.vertices
            ]
            set_surface_coords(srf, Polygon3D(new_vertices))

set_default_constructions(doc, construction_name='Default Construction')

Assign a placeholder construction to surfaces that lack one.

Iterates all BuildingSurface:Detailed and FenestrationSurface:Detailed objects and sets construction_name for any whose current value is empty or None.

Does not create the Construction object itself — the caller is responsible for ensuring it exists.

Parameters:

Name Type Description Default
doc IDFDocument

The document to modify.

required
construction_name str

Name of the construction to assign.

'Default Construction'

Returns:

Type Description
int

Number of surfaces updated.

Source code in src/idfkit/geometry_builders.py
def set_default_constructions(doc: IDFDocument, construction_name: str = "Default Construction") -> int:
    """Assign a placeholder construction to surfaces that lack one.

    Iterates all ``BuildingSurface:Detailed`` and
    ``FenestrationSurface:Detailed`` objects and sets
    ``construction_name`` for any whose current value is empty or ``None``.

    Does **not** create the ``Construction`` object itself — the caller
    is responsible for ensuring it exists.

    Args:
        doc: The document to modify.
        construction_name: Name of the construction to assign.

    Returns:
        Number of surfaces updated.
    """
    count = 0
    for stype in ("BuildingSurface:Detailed", "FenestrationSurface:Detailed"):
        for srf in doc[stype]:
            if not srf.get("Construction Name"):
                srf.construction_name = construction_name
                count += 1
    return count

See Also

  • Zoning -- create_building, core-perimeter zoning, footprint helpers, and multi-zone building generation
  • Geometry -- Lower-level 3D primitives, coordinate transforms, and surface intersection
  • Visualization -- 3D rendering of building geometry
  • Thermal -- R/U-value calculations for constructions