← Skills

SIP Roof Construction

Native Read-only

Mono-pitch, duo-pitch, and flat roof construction code, ridge bevels, gable sections, and flat roof drainage. Injected to agents building roof components.

/skills/sip_roofs.md

Estimated tokens
7191
Characters
28763
Source
Native

Markdown

# SIP Roof Construction Skill

This skill defines how to design and model SIP roof components in FreeCAD using the direct modeling approach. It covers roof panel specifications, ridge bevels, mono-pitch and duo-pitch roofs, gable sections, flat roof drainage, and span splitting — all in metric units.

---

## SIP Panel Specifications (Metric)

All dimensions in millimetres. OSB facing is 11mm each side.

| Name         | Total Thickness | Core (EPS) | Approx. R-Value |
|--------------|-----------------|------------|-----------------|
| SIP-100      | 122mm           | 100mm      | R-15            |
| SIP-150      | 172mm           | 150mm      | R-23            |
| SIP-200      | 222mm           | 200mm      | R-30            |
| SIP-250      | 272mm           | 250mm      | R-38            |
| SIP-300      | 322mm           | 300mm      | R-45            |

**Standard panel widths:** 1200mm (preferred) or 1220mm
**Standard panel heights:** 2400mm, 2700mm, 3000mm (custom heights available)
**OSB face thickness:** 11mm each side

Wall panels orient with height vertical (Z-axis). Roof panels orient with height along slope.

---

## Roof Panels

SIP roof panels are the same composite construction as wall panels but oriented at the roof pitch. The slope is defined by the pitch angle.

### Ridge Bevel — CRITICAL for Watertight Fit

Without a bevel cut at the ridge, the two slope panels overrun the centreline and cannot mate flush. Always cut the bevel in the flat panel before applying the placement rotation.

**Panel local frame (all pitched roof code uses this):**
- X = 0 to BUILDING_WIDTH (along ridge)
- Y = 0 to slope_length (along slope, 0 = eave end, slope_length = ridge end)
- Z = 0 to TOTAL_THICKNESS (0 = interior/bottom face, TT = exterior/top face)

**Rule:** cut a triangular prism from the ridge end (Y = slope_length) in the YZ plane, extruded along X. After rotating the flat panel to pitch angle, this cut face becomes plumb (vertical), and the two slope faces mate flush at the apex.

```
Flat panel cross-section in YZ plane (before rotation):
 ←── slope_length ──────────────── bevel ──┐
 ──────────────────────────────────────────│  Z = 0  (interior face)
                                            │
 ──────────────────────────────╱───────────│  Z = TT (exterior face)
                               ↑
               ridge end: exterior face is cut back by bevel_y = TT × tan(pitch)
               interior face stays at Y = slope_length
```

**Formulae:**
- `bevel_y = TOTAL_THICKNESS * tan(PITCH_DEG)` — ridge bevel setback at exterior face
- `eave_bevel_y = TOTAL_THICKNESS / tan(PITCH_DEG)` — eave bevel setback at interior face

```python
import math, Part
from FreeCAD import Vector, Placement, Rotation

PITCH_RAD = math.radians(PITCH_DEG)

# Panel builder: X = ridge_length, Y = slope_length, Z = thickness
def make_slope_panel(ridge_len, slope_len, face_t, core_t):
    f1 = Part.makeBox(ridge_len, slope_len, face_t)
    c  = Part.makeBox(ridge_len, slope_len, core_t, Vector(0, 0, face_t))
    f2 = Part.makeBox(ridge_len, slope_len, face_t, Vector(0, 0, face_t + core_t))
    return f1.fuse(c).fuse(f2)

# Ridge bevel: cut triangle at Y = slope_length end (YZ plane, extruded in X)
# Triangle: (sl - bevel_y, TT), (sl, TT), (sl, 0) — removes exterior overhang
def cut_ridge_bevel_slope(panel, sl, bevel_y, ridge_len, tt):
    pts = [Vector(0, sl - bevel_y, tt),
           Vector(0, sl,           tt),
           Vector(0, sl,            0),
           Vector(0, sl - bevel_y, tt)]
    return panel.cut(Part.Face(Part.makePolygon(pts)).extrude(Vector(ridge_len, 0, 0)))

# Eave bevel: cut triangle at Y = 0 end so panel bears on angled-cut top plate
# Triangle: (0, 0), (eave_bv, 0), (0, TT) — removes interior corner at eave
def cut_eave_bevel_slope(panel, eave_bv, ridge_len, tt):
    pts = [Vector(0,       0,  0),
           Vector(0, eave_bv,  0),
           Vector(0,       0, tt),
           Vector(0,       0,  0)]
    return panel.cut(Part.Face(Part.makePolygon(pts)).extrude(Vector(ridge_len, 0, 0)))
```

### Mono-Pitch Roof

Single slope from low eave wall to high wall (or ridge). Uses the same panel local frame as duo-pitch: X = ridge length (BUILDING_WIDTH), Y = along slope, Z = thickness.

```python
# === MONO-PITCH PARAMETERS ===
BUILDING_WIDTH = 6000      # ridge/eave length (X direction)
BUILDING_DEPTH = 4000      # horizontal span from eave to high wall (Y direction)
PITCH_DEG = 20
PANEL_WIDTH_ROOF = 1200
CORE_THICKNESS = 150
FACE_THICKNESS = 11
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS
EAVE_HEIGHT = 2960         # Z of top of low eave wall top plate

PITCH_RAD     = math.radians(PITCH_DEG)
slope_length  = BUILDING_DEPTH / math.cos(PITCH_RAD)   # along-slope distance
rise          = BUILDING_DEPTH * math.tan(PITCH_RAD)    # height gained across span
bevel_y       = TOTAL_THICKNESS * math.tan(PITCH_RAD)   # ridge bevel setback
eave_bevel_y  = TOTAL_THICKNESS / math.tan(PITCH_RAD)   # eave bevel setback

n_panels = math.ceil(slope_length / PANEL_WIDTH_ROOF)

roof_parts = []
y_pos = 0.0
for i in range(n_panels):
    pw = min(PANEL_WIDTH_ROOF, slope_length - y_pos)
    p = make_slope_panel(BUILDING_WIDTH, pw, FACE_THICKNESS, CORE_THICKNESS)
    p.translate(Vector(0, y_pos, 0))
    if i == n_panels - 1:    # ridge/high-wall end bevel
        p = cut_ridge_bevel_slope(p, pw, bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
    if i == 0:               # eave end bevel
        p = cut_eave_bevel_slope(p, eave_bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
    roof_parts.append(p)
    y_pos += pw

roof = roof_parts[0]
for p in roof_parts[1:]:
    roof = roof.fuse(p)

# Rotation by +PITCH_DEG around X tilts the Y (slope) axis upward into +Z.
# Base places the eave bottom corner (local 0,0,0) at the inner face of the low wall top plate.
rot = Rotation(Vector(1, 0, 0), PITCH_DEG)
pl  = Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot)
roof_obj          = doc.addObject("Part::Feature", "Roof")
roof_obj.Shape    = roof
roof_obj.Placement = pl
```

### Duo-Pitch (Gable) Roof

Two slopes meeting at a central ridge beam. Each slope is built in the SAME local frame and gets a ridge bevel so the faces mate flush at the apex. South and north placements differ — do not copy one from the other with a simple sign flip.

**Orientation convention:** ridge runs along BUILDING_WIDTH (X), slope spans BUILDING_DEPTH (Y). South slope eave at Y ≈ PANEL_THICKNESS; north slope eave at Y ≈ BUILDING_DEPTH − PANEL_THICKNESS; ridge at Y = BUILDING_DEPTH/2.

**Panel local frame:** X = 0…BUILDING_WIDTH (ridge direction), Y = 0…slope_length (slope, 0 = eave, slope_length = ridge end), Z = 0…TOTAL_THICKNESS (0 = interior/bottom face, TT = exterior/top face).

```python
# === DUO-PITCH PARAMETERS ===
HALF_SPAN    = BUILDING_DEPTH / 2          # horizontal half-span (eave to ridge centreline)
slope_length = HALF_SPAN / math.cos(PITCH_RAD)
ridge_height = HALF_SPAN * math.tan(PITCH_RAD)
RIDGE_Z      = EAVE_HEIGHT + ridge_height

# Ridge bevel: cut wedge from ridge end (Y = slope_length) so the face becomes
# vertical after the slope is rotated to pitch.  bevel_y = TT * tan(P)
bevel_y      = TOTAL_THICKNESS * math.tan(PITCH_RAD)

# Eave bevel: cut wedge from eave end (Y = 0) so the panel bears cleanly on the
# angled-cut top plate.  eave_bevel_y = TT / tan(P) = TT * cot(P)
eave_bevel_y = TOTAL_THICKNESS / math.tan(PITCH_RAD)

# --- Panel builder (X = ridge, Y = slope, Z = thickness) ---
def make_slope_panel(bw, sl, face_t, core_t):
    f1 = Part.makeBox(bw, sl, face_t)
    c  = Part.makeBox(bw, sl, core_t,  Vector(0, 0, face_t))
    f2 = Part.makeBox(bw, sl, face_t,  Vector(0, 0, face_t + core_t))
    return f1.fuse(c).fuse(f2)

# Ridge bevel cut — removes triangle at Y = slope_length end (in YZ plane, extruded along X)
def cut_ridge_bevel_slope(panel, sl, bv_y, bw, tt):
    pts = [Vector(0, sl - bv_y, tt),
           Vector(0, sl,        tt),
           Vector(0, sl,         0),
           Vector(0, sl - bv_y, tt)]
    face = Part.Face(Part.makePolygon(pts))
    return panel.cut(face.extrude(Vector(bw, 0, 0)))

# Eave bevel cut — removes triangle at Y = 0 end
def cut_eave_bevel_slope(panel, eb_y, bw, tt):
    pts = [Vector(0,     0,  0),
           Vector(0, eb_y,   0),
           Vector(0,     0, tt),
           Vector(0,     0,  0)]
    face = Part.Face(Part.makePolygon(pts))
    return panel.cut(face.extrude(Vector(bw, 0, 0)))

# --- Build south slope ---
n_panels = math.ceil(slope_length / PANEL_WIDTH_ROOF)
south_parts = []
y_pos = 0.0
for i in range(n_panels):
    pw = min(PANEL_WIDTH_ROOF, slope_length - y_pos)
    p = make_slope_panel(BUILDING_WIDTH, pw, FACE_THICKNESS, CORE_THICKNESS)
    p.translate(Vector(0, y_pos, 0))
    if i == n_panels - 1:
        p = cut_ridge_bevel_slope(p, pw, bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
    if i == 0:
        p = cut_eave_bevel_slope(p, eave_bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
    south_parts.append(p)
    y_pos += pw

south_roof = south_parts[0]
for p in south_parts[1:]:
    south_roof = south_roof.fuse(p)

# South placement: +PITCH_DEG around X tilts the Y (slope) axis up toward +Z.
# Base puts the eave bottom corner (local 0,0,0) at the inner face of south wall top.
rot_s = Rotation(Vector(1, 0, 0), PITCH_DEG)
pl_s  = Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot_s)
south_obj          = doc.addObject("Part::Feature", "RoofSouthSlope")
south_obj.Shape    = south_roof
south_obj.Placement = pl_s

# --- North slope (same panel geometry, different placement) ---
north_roof = south_roof.copy()

# North placement: (180 - PITCH_DEG) around X maps local +Y to world -Y direction,
# so Y=0 (eave end) stays at the north wall inner face and Y=slope_length (ridge end)
# reaches world Y = BUILDING_DEPTH/2.  Do NOT use -PITCH_DEG — that sends the ridge
# further away from centre, not toward it.
rot_n = Rotation(Vector(1, 0, 0), 180 - PITCH_DEG)
pl_n  = Placement(Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, EAVE_HEIGHT), rot_n)
north_obj          = doc.addObject("Part::Feature", "RoofNorthSlope")
north_obj.Shape    = north_roof
north_obj.Placement = pl_n

# --- Ridge beam (LVL or glulam, vertical, centred at ridge) ---
RIDGE_BEAM_D = max(200, int(HALF_SPAN / 10))   # depth (Z), min 200mm
RIDGE_BEAM_W = 90                               # width (Y)
ridge = Part.makeBox(
    BUILDING_WIDTH,
    RIDGE_BEAM_W,
    RIDGE_BEAM_D,
    Vector(0, BUILDING_DEPTH / 2 - RIDGE_BEAM_W / 2, RIDGE_Z)
)
ridge_obj       = doc.addObject("Part::Feature", "RidgeBeam")
ridge_obj.Shape = ridge
```

**Key placement sanity check (substitute real numbers to verify):**
- South eave bottom at world (x, PANEL_THICKNESS, EAVE_HEIGHT) ✓
- South ridge bottom at world (x, PANEL_THICKNESS + HALF_SPAN, RIDGE_Z) ≈ (x, BUILDING_DEPTH/2, RIDGE_Z) ✓
- North eave bottom at world (x, BUILDING_DEPTH − PANEL_THICKNESS, EAVE_HEIGHT) ✓
- North ridge bottom at world (x, BUILDING_DEPTH/2 + PANEL_THICKNESS − small, RIDGE_Z) ≈ (x, BUILDING_DEPTH/2, RIDGE_Z) ✓

### Gable End Wall — Triangular Section

For a duo-pitch roof (ridge along X), the **east and west walls** are gable end walls. They must include the full triangular gable section above EAVE_HEIGHT. Build each gable wall component as two fused solids:

1. **Rectangular base** — standard SIP panel loop from Z=0 to Z=EAVE_HEIGHT (full BUILDING_DEPTH length)
2. **Triangular gable prism** — above EAVE_HEIGHT, triangular cross-section in the YZ plane, extruded PANEL_THICKNESS in X

```python
# Triangular gable section for east or west gable end wall
# Cross-section (YZ plane) triangle:
#   (Y=0,                Z=EAVE_HEIGHT)   ← south corner (eave height)
#   (Y=BUILDING_DEPTH/2, Z=RIDGE_Z)       ← apex (ridge height)
#   (Y=BUILDING_DEPTH,   Z=EAVE_HEIGHT)   ← north corner (eave height)

gable_pts = [
    Vector(0,  0,                  EAVE_HEIGHT),
    Vector(0,  BUILDING_DEPTH / 2, RIDGE_Z),
    Vector(0,  BUILDING_DEPTH,     EAVE_HEIGHT),
    Vector(0,  0,                  EAVE_HEIGHT),   # close
]
gable_wire = Part.makePolygon(gable_pts)
gable_face = Part.Face(gable_wire)
gable_prism = gable_face.extrude(Vector(PANEL_THICKNESS, 0, 0))

# West gable wall (exterior face at X = 0):
# The rectangular base wall is already built in the wall loop at X=0.
# Fuse the gable prism on top — it sits at X=0 to X=PANEL_THICKNESS, Y=0 to BUILDING_DEPTH.
west_gable = wall_solid.fuse(gable_prism)   # wall_solid = rectangular section

# East gable wall (exterior face at X = BUILDING_WIDTH):
gable_prism_east = gable_prism.copy()
gable_prism_east.translate(Vector(BUILDING_WIDTH - PANEL_THICKNESS, 0, 0))
east_gable = wall_solid_east.fuse(gable_prism_east)
```

**The angled top face of the gable prism naturally matches the roof pitch** — no additional cut needed. The face slope from (0, EAVE_HEIGHT) to (BUILDING_DEPTH/2, RIDGE_Z) is `rise / run = ridge_height / HALF_SPAN = tan(PITCH_DEG)` ✓

### Flat Roof

A flat roof uses horizontal SIP panels resting directly on the wall top plates. Use SIP-200 or SIP-250 for flat roofs — the additional insulation compensates for the reduced stack effect and improves thermal performance at the cold deck.

**Drainage fall is not optional — a truly flat surface ponds water and will fail.** The slope is created by tapered insulation on top of the horizontal SIP deck; the SIP deck itself stays level. Do not tilt the structural panels — it makes wall height coordination impossible.

**Fall rates:**
- **1:40 (25mm/m)** — absolute code minimum (BS 8217). Produces ~40mm height difference over a 1.6m inner span; barely visible in a 3D model and leaves no margin for deflection or construction tolerance.
- **1:20 (50mm/m)** — **design target.** Use this for all models. Produces ~80mm height difference over a 1.6m inner span, clearly visible, and ensures drainage even after some deflection occurs in service.

**Always use 1:20 in generated models.** The formula is `FALL_HEIGHT = INNER_DEPTH / 20`.

#### Flat Roof Elements

1. **SIP roof panels** — horizontal, spanning the shorter building dimension. Multiple 1200mm-wide panels joined by splines, same panel-loop pattern as walls.
2. **Tapered insulation board** — modelled as a **wedge** (thick at high side, thin at low side) using a polygon-face extrude. Use `FALL_RATIO = 20` (1:20 design fall). Minimum thickness at the low point: **50mm** (absolute code floor is 18mm but 50mm gives visible slope and prevents ponding in construction tolerances).
3. **Waterproof membrane** — EPDM or TPO, 3mm shell on top of the insulation.
4. **Parapet walls** — short SIP-100 panels extending above roof level on all four sides. The interior face of the parapet is lined with the membrane upstand.
5. **Scupper outlets through the parapet** — rectangular holes cut through the parapet panel at the low side of the fall. These are the only way water can leave a parapeted roof. Without scuppers the roof will flood.
6. **Overflow scuppers** — a second set of scupper holes 50mm higher than the primary, as a backup if the primary blocks.
7. **Downpipe stubs** — short cylinder representing the external downpipe at each primary scupper.
8. **Flat roof bearer (optional)** — for spans > 3000mm, a mid-span LVL bearer under the panels reduces deflection.

#### Parapet vs. Open Eave

| Detail       | Parapet                          | Open Eave / Fascia             |
|--------------|----------------------------------|--------------------------------|
| Look         | Modern, clean — walls continue past roof | Reveals roof edge from below   |
| Water        | Contained; internal drainage     | Drains freely at edge          |
| Model        | Extend wall panels up by PARAPET_H above roof | Fascia board at panel end      |
| Default      | **Use this for modern flat roofs** | Use for agricultural/industrial |

#### Flat Roof Code

```python
import FreeCAD, Part, math
from FreeCAD import Vector

doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("FlatRoof")

# === PARAMETERS ===
BUILDING_WIDTH = 3000      # outer face to outer face (X)
BUILDING_DEPTH = 2000      # outer face to outer face (Y)
WALL_HEIGHT = 2970         # BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H
PANEL_THICKNESS = 172      # wall SIP total thickness

ROOF_CORE = 200            # SIP-200 for flat roofs
ROOF_FACE = 11
ROOF_THICKNESS = ROOF_CORE + 2 * ROOF_FACE   # 222mm
ROOF_PANEL_W = 1200

INSUL_MIN  = 50            # insulation at low point (mm) — 50mm design minimum; code floor is 18mm
FALL_RATIO = 20            # 1:20 design fall — use this, not 1:40 (code minimum only)
MEMBRANE_T = 3             # EPDM/TPO membrane

PARAPET_H = 300            # parapet height above ROOF_Z
PARAPET_T = 122            # SIP-100 parapet panels
COPING_T = 3               # aluminium coping

# === DRAINAGE GEOMETRY ===
# Fall direction: high side at Y = BUILDING_DEPTH - PANEL_THICKNESS (back/north)
#                low side at Y = PANEL_THICKNESS (front/south) — scuppers here
INNER_WIDTH = BUILDING_WIDTH - 2 * PANEL_THICKNESS
INNER_DEPTH = BUILDING_DEPTH - 2 * PANEL_THICKNESS
FALL_HEIGHT = INNER_DEPTH / FALL_RATIO       # 1:20 design fall — e.g. 80mm over 1600mm inner depth
INSUL_HIGH  = INSUL_MIN + FALL_HEIGHT        # thickness at high side
# Confirm the slope is visible: FALL_HEIGHT should be >> 0
# e.g. 3000×2000 building: INNER_DEPTH≈1656mm → FALL_HEIGHT≈83mm, INSUL range 50→133mm
ROOF_Z = WALL_HEIGHT                         # bottom face of SIP roof panels

insul_z_base = ROOF_Z + ROOF_THICKNESS       # top of roof deck = base of insulation

# === STEP 1: SIP Roof Deck (multiple panels — same loop as walls) ===
full_panels = INNER_DEPTH // ROOF_PANEL_W
remainder = INNER_DEPTH % ROOF_PANEL_W
panel_widths_roof = [ROOF_PANEL_W] * full_panels + ([remainder] if remainder > 0 else [])

roof_parts = []
y = PANEL_THICKNESS
for i, pw in enumerate(panel_widths_roof):
    f1 = Part.makeBox(INNER_WIDTH, pw, ROOF_FACE,
                      Vector(PANEL_THICKNESS, y, ROOF_Z))
    co = Part.makeBox(INNER_WIDTH, pw, ROOF_CORE,
                      Vector(PANEL_THICKNESS, y, ROOF_Z + ROOF_FACE))
    f2 = Part.makeBox(INNER_WIDTH, pw, ROOF_FACE,
                      Vector(PANEL_THICKNESS, y, ROOF_Z + ROOF_FACE + ROOF_CORE))
    roof_parts.append(f1.fuse(co).fuse(f2))
    if i < len(panel_widths_roof) - 1:
        sp = Part.makeBox(INNER_WIDTH, 45, 90,
                          Vector(PANEL_THICKNESS, y + pw - 22.5,
                                 ROOF_Z + (ROOF_THICKNESS - 90) / 2))
        roof_parts.append(sp)
    y += pw

roof_deck = roof_parts[0]
for p in roof_parts[1:]:
    roof_deck = roof_deck.fuse(p)

# === STEP 2: Tapered insulation — WEDGE, not a flat box ===
# Profile in Y-Z plane, extruded in X.
# Low side (front, Y = PANEL_THICKNESS): insul_z_base + INSUL_MIN
# High side (back, Y = BUILDING_DEPTH - PANEL_THICKNESS): insul_z_base + INSUL_HIGH
insul_pts = [
    Vector(PANEL_THICKNESS, PANEL_THICKNESS,                      insul_z_base),
    Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS,     insul_z_base),
    Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS,     insul_z_base + INSUL_HIGH),
    Vector(PANEL_THICKNESS, PANEL_THICKNESS,                      insul_z_base + INSUL_MIN),
    Vector(PANEL_THICKNESS, PANEL_THICKNESS,                      insul_z_base),
]
insul_wire = Part.makePolygon(insul_pts)
insul_face = Part.Face(insul_wire)
tapered_insul = insul_face.extrude(Vector(INNER_WIDTH, 0, 0))

# === STEP 3: Membrane — wedge following insulation slope, same profile in Y-Z plane ===
# Must be a wedge, not a flat box, so the finished roof surface reads as sloped in the model.
mem_pts = [
    Vector(PANEL_THICKNESS, PANEL_THICKNESS,                      insul_z_base + INSUL_MIN),
    Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS,     insul_z_base + INSUL_HIGH),
    Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS,     insul_z_base + INSUL_HIGH  + MEMBRANE_T),
    Vector(PANEL_THICKNESS, PANEL_THICKNESS,                      insul_z_base + INSUL_MIN   + MEMBRANE_T),
    Vector(PANEL_THICKNESS, PANEL_THICKNESS,                      insul_z_base + INSUL_MIN),
]
mem_wire = Part.makePolygon(mem_pts)
mem_face = Part.Face(mem_wire)
membrane = mem_face.extrude(Vector(INNER_WIDTH, 0, 0))

# === STEP 4: Parapet walls (four sides) ===
PARAPET_Z = ROOF_Z   # parapets start at same Z as roof panels (wall top)
PARAPET_TOP_Z = PARAPET_Z + PARAPET_H

par_s = Part.makeBox(BUILDING_WIDTH, PARAPET_T, PARAPET_H,
                     Vector(0, 0, PARAPET_Z))
par_n = Part.makeBox(BUILDING_WIDTH, PARAPET_T, PARAPET_H,
                     Vector(0, BUILDING_DEPTH - PARAPET_T, PARAPET_Z))
par_w = Part.makeBox(PARAPET_T, BUILDING_DEPTH - 2 * PARAPET_T, PARAPET_H,
                     Vector(0, PARAPET_T, PARAPET_Z))
par_e = Part.makeBox(PARAPET_T, BUILDING_DEPTH - 2 * PARAPET_T, PARAPET_H,
                     Vector(BUILDING_WIDTH - PARAPET_T, PARAPET_T, PARAPET_Z))

# === STEP 5: Scuppers through south parapet at low side — MANDATORY ===
# Primary scupper: centred on south parapet, at membrane level
SCUPPER_W  = 100    # opening width
SCUPPER_H  = 75     # opening height
SCUPPER_Z  = insul_z_base + INSUL_MIN - 10   # just below low-point membrane surface

scupper_primary = Part.makeBox(
    SCUPPER_W, PARAPET_T, SCUPPER_H,
    Vector(BUILDING_WIDTH / 2 - SCUPPER_W / 2, 0, SCUPPER_Z)
)
par_s = par_s.cut(scupper_primary)

# Overflow scupper: offset to one side, 50mm higher (backup if primary blocks)
scupper_overflow = Part.makeBox(
    SCUPPER_W, PARAPET_T, SCUPPER_H,
    Vector(BUILDING_WIDTH / 4 - SCUPPER_W / 2, 0, SCUPPER_Z + 50)
)
par_s = par_s.cut(scupper_overflow)

# === STEP 6: Downpipe stub at primary scupper (exterior face) ===
DOWNPIPE_R = 50    # 100mm dia downpipe
downpipe = Part.makeCylinder(
    DOWNPIPE_R, 400,
    Vector(BUILDING_WIDTH / 2, -400, SCUPPER_Z + SCUPPER_H / 2 - DOWNPIPE_R)
)

# === STEP 7: Aluminium coping cap on parapet top ===
cope_s = Part.makeBox(BUILDING_WIDTH,     PARAPET_T + 50, COPING_T, Vector(0,                        -25, PARAPET_TOP_Z))
cope_n = Part.makeBox(BUILDING_WIDTH,     PARAPET_T + 50, COPING_T, Vector(0,                        BUILDING_DEPTH - PARAPET_T - 25, PARAPET_TOP_Z))
cope_w = Part.makeBox(PARAPET_T + 50,     BUILDING_DEPTH, COPING_T, Vector(-25,                      0, PARAPET_TOP_Z))
cope_e = Part.makeBox(PARAPET_T + 50,     BUILDING_DEPTH, COPING_T, Vector(BUILDING_WIDTH - PARAPET_T - 25, 0, PARAPET_TOP_Z))

# === ADD TO DOCUMENT ===
for shape, name in [
    (roof_deck,    "RoofDeck"),
    (tapered_insul,"TaperedInsulation"),
    (membrane,     "Membrane"),
    (par_s,        "ParapetSouth"),
    (par_n,        "ParapetNorth"),
    (par_w,        "ParapetWest"),
    (par_e,        "ParapetEast"),
    (downpipe,     "Downpipe"),
    (cope_s,       "CopingSouth"),
    (cope_n,       "CopingNorth"),
    (cope_w,       "CopingWest"),
    (cope_e,       "CopingEast"),
]:
    obj = doc.addObject("Part::Feature", name)
    obj.Shape = shape

doc.recompute()
if FreeCAD.GuiUp:
    FreeCAD.Gui.ActiveDocument.ActiveView.fitAll()
```

---

### Roof Eave Blocking

At the eave, the panel foam end is exposed. Solid timber blocking closes the gap and provides a nailing surface for fascia.

```python
EAVE_BLOCK_H = TOTAL_THICKNESS
eave_block = Part.makeBox(BUILDING_DEPTH, EAVE_BLOCK_H, 90,
                          Vector(0, 0, EAVE_HEIGHT - 90))
```

---

## MANDATORY: Panel Span Splitting

Standard SIP stock is 2440mm × 1220mm. The 1200mm width rule (side-by-side panels) is already required. The **span direction** (perpendicular to the panel seams) has the same 2440mm limit and is equally non-negotiable.

**When the rule fires:** Whenever the span dimension of any panel — roof run or any other — exceeds 2440mm.

**Non-negotiable:** A panel longer than 2440mm cannot be ordered from standard stock. The design cannot be built. Split every time, without exception.

### The Split Algorithm

```python
import math

MAX_SPAN_MM = 2440

def span_sections(total_span):
    """Return list of equal section lengths, each ≤ 2440mm."""
    n = math.ceil(total_span / MAX_SPAN_MM)
    section = total_span / n          # float — equal sections
    return [section] * n              # n sections, all under 2440mm
```

Use **equal sections** (not 2440 + remainder). Equal sections avoid a tiny sliver panel and produce a cleaner, more buildable result.

### Roof Span Splice (flat roof, most common case)

Roof panels run in the SPAN direction. For a building with INNER_WIDTH > 2440mm:

```python
import math, FreeCAD, Part
from FreeCAD import Vector

# === PARAMETERS ===
BUILDING_WIDTH  = 3000    # outer face to outer face (X)
PANEL_THICKNESS = 122     # wall SIP total thickness
CORE_THICKNESS  = 100     # foam core depth
FACE_THICKNESS  = 11      # OSB skin
ROOF_PANEL_W    = 1200    # panel width (Y direction, side-by-side)
LVL_W           = 45      # LVL bearer width (in span direction)
MAX_SPAN        = 2440    # stock sheet max span

INNER_WIDTH = BUILDING_WIDTH - 2 * PANEL_THICKNESS   # 2756mm example → 2512mm

# How many span sections?
n_span = math.ceil(INNER_WIDTH / MAX_SPAN)            # 2512/2440 → ceil(1.03) = 2
SECTION_SPAN = INNER_WIDTH / n_span                   # 2512 / 2 = 1256mm (fits easily)
# → 2 sections of 1256mm, 1 LVL bearer between them

# Roof panel loop (width direction, Y) × span sections (X direction)
# Build in span sections first, then tile in the width direction
roof_parts = []

for i_width, pw in enumerate(panel_widths):          # panel_widths = 1200mm buckets in Y
    y = PANEL_THICKNESS + sum(panel_widths[:i_width])
    x = PANEL_THICKNESS

    for i_span in range(n_span):
        # Panel section
        sx = SECTION_SPAN
        f1 = Part.makeBox(sx, pw, FACE_THICKNESS, Vector(x, y, ROOF_Z))
        co = Part.makeBox(sx, pw, CORE_THICKNESS, Vector(x, y, ROOF_Z + FACE_THICKNESS))
        f2 = Part.makeBox(sx, pw, FACE_THICKNESS, Vector(x, y, ROOF_Z + FACE_THICKNESS + CORE_THICKNESS))
        roof_parts.append(f1.fuse(co).fuse(f2))

        # LVL span bearer between this section and the next
        if i_span < n_span - 1:
            bearer = Part.makeBox(
                LVL_W, pw, CORE_THICKNESS,
                Vector(x + sx - LVL_W / 2, y, ROOF_Z + FACE_THICKNESS)
            )
            roof_parts.append(bearer)

        x += sx
```

**Worked examples:**

| Building width | INNER_WIDTH | n sections | Section span | LVL bearers |
|---------------|-------------|------------|--------------|-------------|
| 2m (INNER~1756) | 1756mm | 1 | 1756mm | 0 — no split needed |
| 3m (INNER~2512) | 2512mm | 2 | 1256mm | 1 mid-span bearer |
| 4m (INNER~3756) | 3756mm | 2 | 1878mm | 1 mid-span bearer |
| 5m (INNER~4756) | 4756mm | 2 | 2378mm | 1 mid-span bearer |
| 6m (INNER~5756) | 5756mm | 3 | 1919mm | 2 bearers at 1/3 and 2/3 span |

### Mono-Pitch / Gable Roof Panel Span

The same algorithm applies. The "span" is measured along the slope (rafter direction). For a mono-pitch:

```python
SLOPE_LENGTH = BUILDING_DEPTH / math.cos(PITCH_RAD)   # slant length along roof surface
n_span = math.ceil(SLOPE_LENGTH / MAX_SPAN)
section_span = SLOPE_LENGTH / n_span
```

---

## ⚠ Non-Negotiable Roof Rules

4. **Flat roofs MUST have a visible drainage fall of 1:20 and scupper outlets.**
   Use `FALL_HEIGHT = INNER_DEPTH / 20` and `INSUL_MIN = 50`. Tapered insulation MUST be a polygon-face wedge (not a flat box), and the membrane on top MUST also be a matching wedge so the finished roof surface reads as sloped. Scupper openings MUST be cut through the parapet at the low side. A flat roof with no scuppers behind a parapet will flood. Never use `INSUL_MIN = 18` or `FALL_RATIO = 40`.

6. **Any panel span exceeding 2440mm MUST be split into multiple sections with LVL bearers.**
   Standard SIP stock sheets are 2440mm long. Split the span into n equal sections where n = ceil(span / 2440), and model an LVL bearer (45mm wide × panel core depth) at every splice point.

9. **Gable end walls MUST include the full triangular gable section above eave height.**
   For a duo-pitch roof with the ridge running along BUILDING_WIDTH (X axis), the EAST and WEST walls are gable end walls. Each gable end wall component MUST include: (a) the rectangular SIP wall section from Z=0 to Z=EAVE_HEIGHT, AND (b) a triangular prism from Z=EAVE_HEIGHT to Z=RIDGE_Z.

10. **Duo-pitch roof panels MUST use the correct placement formula.**
    South slope: `Rotation(Vector(1,0,0), PITCH_DEG)`, base `Vector(0, PANEL_THICKNESS, EAVE_HEIGHT)`. North slope: `Rotation(Vector(1,0,0), 180 - PITCH_DEG)`, base `Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, EAVE_HEIGHT)`. Using `Rotation(X, -PITCH_DEG)` for north is wrong.
v0.0.170