← Skills

SIP Construction (DEPRECATED)

Native Read-only

DEPRECATED — superseded by the ElixiFree-API SIP skills (sip_decomposition, sip_walls_elixifree, sip_roofs, sip_foundations, sip_openings). Retained for backward compatibility; do not attach to new projects.

/skills/depreciated/sip_building.md

Estimated tokens
23013
Characters
92052
Source
Native

Markdown

# Structural Insulated Panel (SIP) Building Skill

This skill defines how to design and model SIP-based buildings in FreeCAD using the direct modeling approach. It covers panel specifications, connections, roofs, foundations, siding, and openings — all in metric units.

**Status note (2026-07-05):** this file predates the assembly-layer placement
redesign. Two conventions below are superseded and must NOT be followed for
building-graph SIP projects:
- **Do NOT call `.orient("Y")`** on a component-generation wall/partition
  script. Pipeline scripts are expanded through `Wall.construct()`, which
  always builds panels span-along-local-X regardless of compass direction
  (`Wall._orientation` is stored but never read by `_WallConstructor`); the
  assembly Placer applies the correct world rotation for every wall.
  `.orient("Y")` only affects the separate, non-pipeline `.build()` whole-wall
  shape and would double-rotate the wall if combined with the Placer's own
  yaw. `.orient()` remains valid Python API for a standalone one-off
  `.build()` script with no Placer.
- **Do NOT set `feature.Placement` in a wall/partition/roof-slope/foundation
  component script.** The assembly Placer owns ALL world position and
  rotation; see `sip_walls_elixifree.md` and `docs/design/SIP_ROOF_STANDARDS.md`
  for the current convention (`.build()`/`.construct()` in the local/default
  frame, no `feature.Placement`, no `.orient()`).

For current pipeline behaviour, prefer `sip_walls_elixifree.md`,
`sip_roofs.md`, `sip_partition_walls.md`, and `sip_foundations.md`.

---

## When to Use This Skill

Use this skill when the project involves SIP construction: residential buildings, cabins, garden rooms, studios, or any structure where walls and roof are built from composite foam-core panels. If a component description mentions "SIP", "structural insulated panel", "wall panel", "roof panel", "spline", or "sill plate" in a building context, apply this skill.

---

## ⚠ Non-Negotiable Construction Rules

These rules apply to every SIP component without exception. Violating them produces a structurally incorrect or unbuilable model.

1. **Walls are always multiple panels — never a single box.**
   Every wall MUST be built using the panel loop (individual 1200mm-wide SIP panels with block splines between them). A wall modelled as one `Part.makeBox` is wrong regardless of wall length.

2. **Every wall component generates its own bottom plate.**
   The wall component (not the foundation) creates a 45×90mm PT timber bottom plate at Z=0 as the very first solid. The SIP panels sit on top of it at Z=90. A wall whose panels start at Z=0 is missing its bottom plate.

3. **Openings described in the brief MUST be modelled with a full-depth timber buck AND a unit.**
   If the brief mentions a window or door on a wall, that wall component MUST: (a) use `make_panel_zone` to stop SIP panels at the king stud face — never cut a void from a continuous wall, (b) add full-depth king studs (`KING_D = TOTAL_THICKNESS`) + full-depth trimmer studs + LVL header + window sill, and (c) add a separate feature for the glazing or door unit. King studs that are only 90mm deep are wrong — they leave the SIP foam unsupported and create a structural gap.

4. **NEVER generate a void component. Void components are banned.**
   A component whose `agent_id` or `role` contains "void", "subtraction", or "boolean tool" MUST NOT appear in any SIP manifest. There is no such thing as a void agent in a SIP building. Openings are always handled inside the wall agent that contains them — a wall with a window is one agent, not a wall agent plus a void agent. If you are about to generate a component named "East Window Void", "Door Void", or similar, stop and instead add the opening to the description of the wall agent it belongs to.

5. **Flat roofs MUST have a drainage fall of 2–5°, created by tapered top plates — never by tapered insulation or a tapered deck.**
   The structural SIP deck is ALWAYS a flat panel and never tapers. The fall is
   borne by a **tapered top plate** on the fall-run (eave) walls: a wedge with a
   level underside and a top face cut to the fall angle. The flat deck rests on
   this wedge and so sits tilted at the fall — its top surface sheds water. The
   walls themselves stay level-topped; the taper lives entirely in the plate.
   This is the SAME Fig 1.4.7 angled eave-plate mechanism used for pitched
   eaves, at a shallower angle (≤5°). **You do NOT author the tapered plate in
   the component script — the constructability layer adds it automatically** to
   the fall-run eave walls of every flat roof. Your job is only to build the
   flat `RoofPanel` deck and the level-topped walls. Never model tapered
   insulation, a tapered deck, `INSUL_MIN`, or `FALL_RATIO` — the fall is
   structural, not an insulation build-up. Scupper openings MUST still be cut
   through the parapet at the low side of the fall — a flat roof with no
   scuppers behind a parapet will flood.

   **Roof edge treatment — parapet vs. cantilevered eave.** A flat roof edge is finished EITHER with a parapet upstand (default) OR, when an overhang is specified, with a **cantilevered eave**. The two are mutually exclusive *per edge* — a parapet sits at the wall line; an overhang projects past it.

   When the brief or the imported "Measured Building Analysis" specifies a **roof overhang** on an edge, that edge MUST be built as a cantilevered eave, NOT a parapet:
   - **Extend the roof SIP deck/panel itself** beyond the wall line by `OVERHANG` on that edge (the same panel runs longer — this is a cantilever, NOT a separate bolt-on component). `ROOF_DECK_LENGTH = span + OVERHANG_eave_1 + OVERHANG_eave_2`.
   - Add a vertical **fascia** board closing the projecting panel end (full panel thickness deep, e.g. 25 × panel-thickness × deck-width) at the outer edge.
   - Add a horizontal **soffit** board closing the underside of the overhang (from wall outer face to fascia inner face).
   - Drain the overhanging edge with a **gutter at the eave** instead of scuppers through a parapet (overhanging edges have no parapet to scupper).
   - **Cantilever limit:** an unsupported roof-SIP cantilever is ≤ ~1/3 of the panel span. If `OVERHANG > span / 3`, add an LVL/I-joist outrigger or double spline at the bearing wall to carry it — do not cantilever an unsupported panel past this limit.

   Edges WITHOUT a specified overhang keep the parapet + scupper detail from the rule above.

6. **Foundation slab extends beyond the wall footprint on all sides.**
   SLAB_W = BUILDING_WIDTH + 2 × SLAB_OVERHANG (minimum 200mm). A slab the same size as the wall footprint leaves no bearing margin.

7. **Any panel span exceeding 2440mm MUST be split into multiple sections with LVL bearers.**
   Standard SIP stock sheets are 2440mm long. A panel component with a span dimension > 2440mm cannot be sourced or built. 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. This applies to roofs AND walls.

8. **Every SIP building MUST include fastener agents for sole plate anchors, roof hurricane ties, and corner post bases.** 
   Fasteners are structural — they are not cosmetic additions. A manifest without fastener agents is incomplete. See the MANDATORY: Fasteners section for required connections, naming conventions, and FreeCAD code patterns.

9. **Sole plates and top plates MUST use CORE_THICKNESS as their depth, offset by FACE_THICKNESS.**
   The timber plate slots into the routed groove in the SIP foam. The OSB skins overhang on both sides. A plate as wide as TOTAL_THICKNESS cannot enter the groove — it is 11mm too wide on each face.

   **Formula:** plate depth = `CORE_THICKNESS`, Y-offset = `FACE_THICKNESS`

   ```python
   # CORRECT — plate fits in the foam channel
   bp = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, BOTTOM_PLATE_H,
                     Vector(0, FACE_THICKNESS, 0))
   tp = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, TOP_PLATE_H,
                     Vector(0, FACE_THICKNESS, BOTTOM_PLATE_H + PANEL_HEIGHT))

   # WRONG — plate is full SIP thickness, cannot slot into panel
   # bp = Part.makeBox(WALL_LENGTH, TOTAL_THICKNESS, BOTTOM_PLATE_H, Vector(0, 0, 0))
   ```

   **Applies to:** bottom plate (sole plate), double top plate, and any sill plate or rim plate that slots into a SIP panel edge.

9. **Gable end walls MUST include the full triangular gable section above eave height, and BUTT between the eave walls' inner faces.**
   For a duo-pitch (gable) roof with the ridge running along BUILDING_WIDTH (X axis), the EAST and WEST walls are gable end walls. Their top is not flat — it follows the roof pitch. Each gable end wall component MUST include: (a) the rectangular SIP wall section from Z=0 to Z=`gable_rake_end_h`, AND (b) a triangular prism from `gable_rake_end_h` up to `underside_ridge_z` (both from `Elixihub.Concept.RoofGeometry` — includes the +90mm eaves-joint lift; see `docs/design/SIP_ROOF_STANDARDS.md` §2.5). A gable wall that stops at eave height leaves an open triangular gap — unenclosed, weather-exposed, and structurally wrong. **The gable's span is `BUILDING_DEPTH - 2*PANEL_THICKNESS`, NOT the full `BUILDING_DEPTH`** — the eave (south/north) walls run through the corners outer-to-outer and the gable end walls butt between their inner faces (corner rule, no exception regardless of relative wall lengths).

10. **Pitched roof slope scripts MUST NOT set `feature.Placement`.** Use `PitchedRoofSlope(...).build()` and `result.add_to_doc("Body")` — stop there. The assembly layer (Placer) applies pitch rotation from the design spec. Setting Placement in the script fights the assembly layer and produces doubled or wrong rotations.

11. **No component-generation script sets `feature.Placement`, and no wall/partition script calls `.orient()`.**
    The assembly Placer owns ALL world position and rotation for every component. `Wall.build()`/`Wall.construct()` place geometry in a default local frame; the Placer applies the correct world position and yaw for every wall regardless of compass direction — `.orient("Y")` only affects a separate, non-pipeline whole-wall `.build()` shape and double-rotates a wall if combined with the Placer's own yaw. Required pattern:
    ```python
    result = Wall(span=..., height=..., stock=...).build()
    result.add_to_doc("Body")
    # No feature.Placement, no .orient() — the Placer handles world position and rotation
    ```

12. **Flat roof deck bottom face MUST be at `Z = WALL_HEIGHT`, never at Z = 0.**
    `ROOF_Z = WALL_HEIGHT` always. A flat roof placed at Z=0 is on the ground, not the roof. Do NOT model the deck as a single `Part.makeBox(BUILDING_WIDTH, BUILDING_DEPTH, THICKNESS)` — use the panel loop from the Flat Roof Code section. The panel loop positions correctly: `Vector(PANEL_THICKNESS, y, ROOF_Z)`.

---

## 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 — wall height, 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 |

### Wall Height Splice (if wall height > 2440mm)

Standard wall heights (2400mm, 2700mm, 3000mm) already come from manufacturer. Heights up to 2700mm fit within a single panel. At 3000mm the panel height is exactly at the 2440mm limit — use a 2440mm lower course + 560mm upper course:

```python
WALL_HEIGHT = 3000    # total panel height needed

if WALL_HEIGHT <= 2440:
    course_heights = [WALL_HEIGHT]    # single course
else:
    n_courses = math.ceil(WALL_HEIGHT / 2440)
    section_h  = WALL_HEIGHT / n_courses
    course_heights = [section_h] * n_courses

# For each course, build the panel loop. Between courses, add a horizontal LVL spline:
# LVL_SPLINE = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, 45,
#                            Vector(0, FACE_THICKNESS, z_splice - 22.5))
```

### 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
```

---

## 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.

---

## Spline Types and Geometry

Splines join adjacent panels at vertical (wall) and lateral (roof) edges.

### Surface Spline
Thin OSB/plywood strip inserted into a routed groove in the panel foam face.
- Width: 45mm
- Thickness: 18mm
- Height: matches panel height
- Sits centred in the panel thickness, recessed 15mm into each panel face

```python
SPLINE_WIDTH = 45
SPLINE_THICKNESS = 18
SPLINE_HEIGHT = PANEL_HEIGHT

spline = Part.makeBox(SPLINE_THICKNESS, SPLINE_WIDTH, SPLINE_HEIGHT,
                      Vector(joint_x - SPLINE_THICKNESS / 2,
                             (TOTAL_THICKNESS - SPLINE_WIDTH) / 2,
                             0))
```

### Block Spline (most common)
Solid timber friction-fitted between panel foam cores at a joint.
- 45 × 90mm for SIP-100/SIP-150 walls
- 45 × 140mm for SIP-200+ walls
- Height: matches panel height

```python
SPLINE_W = 45
SPLINE_D = 90   # or 140 for thicker panels

spline = Part.makeBox(SPLINE_W, SPLINE_D, PANEL_HEIGHT,
                      Vector(joint_x,
                             (TOTAL_THICKNESS - SPLINE_D) / 2,
                             BOTTOM_PLATE_H))
```

### LVL Spline
Engineered lumber for structural or high-load joints. Same geometry as block spline but specified as LVL material. Size to load — minimum 45 × 90mm.

### Double Block Spline (corners)
Two block splines side by side at corner junctions. Used where extra structural capacity is needed at L-corners and wall-to-roof junctions.

---

## Wall Assembly — ALWAYS use `Wall` builder

**Do not write the three-layer panel loop manually.** Use the `Wall` builder — it handles panel segmentation, block splines, sole plate, and top plate internally.

```
┌─────────────────────────────────────┐  ← double top plate  Z = BOTTOM_PLATE_H + PANEL_HEIGHT
│  Panel  │Sp│  Panel  │Sp│  Panel  │  ← SIP panels         Z = BOTTOM_PLATE_H (= 90)
└─────────────────────────────────────┘  ← PT bottom plate   Z = 0
══════════════════════════════════════   ← slab top           Z = 0
```

**Wall height** = BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H = 90 + panel_height + 180mm

```python
from elixifree.domains.sip import Wall
from FreeCAD import Placement, Rotation, Vector

result = Wall(span=WALL_LENGTH, height=PANEL_HEIGHT, stock=STOCK).build()
result.add_to_doc("Body")
# Do NOT set feature.Placement and do NOT call .orient("Y") for a component-generation
# script — see the status note at the top of this file. The assembly Placer owns all
# world position and rotation for every wall regardless of compass direction.
```

**Superseded — for historical reference only.** The `.orient("Y")` /
`feature.Placement` convention below was replaced by the Placer-owns-everything
model; do not follow it for new component scripts:

~~Assembly positions (inner-face-at-origin convention):~~
~~- South wall: `Vector(0, 0, 0)`~~
~~- North wall: `Vector(0, BUILDING_DEPTH, 0)` — NOT `BUILDING_DEPTH - PANEL_THICKNESS`~~
~~- West wall `.orient("Y")`: `Vector(0, 0, 0)`~~
~~- East wall `.orient("Y")`: `Vector(BUILDING_WIDTH, 0, 0)` — NOT `BUILDING_WIDTH - PANEL_THICKNESS`~~

See `sip_walls_elixifree.md` for the current `Wall` API and pipeline convention (`.gable()`, `.opening()`, `.inner_groove()`; `.orient()` is standalone-`.build()`-only, not for pipeline scripts).

---

## Corner Conditions

### L-Corner (external corner)
One wall panel face butts into the exterior face of the perpendicular wall's end panel. A double block spline fills the interior gap. The corner panel sits flush with the exterior face.

```
Plan view (top down):
 ┌──────────────────┐
 │  Wall A panels   │
 │                  ├──┐
 │                  │  │ Wall B
 └──────────────────┘  │ panels
       ↑               │
  double spline here   │
```

- Wall A runs full length to the outer face
- Wall B's end panel butts to Wall A's exterior OSB face
- A double block spline (2× 45×90mm) fills the corner pocket on Wall B's end

### T-Corner (interior partition meeting exterior wall)
Interior wall panel butts into the face of the exterior wall panel. Single block spline at junction. Interior wall bottom plate runs to exterior wall face.

---

## Wall Openings — Windows and Doors

### Construction Principle — Full-Depth Timber Buck

In SIP construction, every window and door opening is framed with a **full-depth timber buck**: a structural timber frame whose depth equals the full wall thickness (`TOTAL_THICKNESS`). The SIP panels stop at the face of the king stud and butt flush against solid timber across the entire wall section. There is no EPS foam in the framing zone — it is entirely replaced by engineered timber.

```
Plan view (top-down cross-section through wall at opening):

 ←─── SIP panel (left) ───→ ←── FRAME_ZONE_W ──────────────→ ←── SIP panel (right) ───→
 [OSB][  EPS core  ][OSB]   [KNG][TRM][  clear void  ][TRM][KNG]   [OSB][  EPS core  ][OSB]
 ←── TOTAL_THICKNESS ──→    ←──── TOTAL_THICKNESS each member ────→  ←── TOTAL_THICKNESS ──→
```

This is why king studs must be **45 × TOTAL_THICKNESS** in cross-section — not 45 × 90mm. A 90mm-deep king stud leaves the SIP panel foam unsupported on either side, creates a thermal bridge gap, and provides no bearing surface for the panel OSB faces.

### Opening Coordinate System

`OPENING_X` is always **the left face of the left king stud** — the X position where SIP panels stop.

```
X:  0 ─────── [panels] ──── OPENING_X
                              ├── king_left   (KING_W = 45mm wide)
                              ├── trimmer_left (TRIMMER_W = 45mm wide)
                              ├── [clear void] (CLEAR_W wide)
                              ├── trimmer_right
                              ├── king_right
                             OPENING_X + FRAME_ZONE_W ──── [panels] ──── WALL_LENGTH
```

### Sizing Calculations

```python
KING_W      = 45                        # king stud face width (X)
KING_D      = TOTAL_THICKNESS           # king stud FULL WALL DEPTH (Y) — not 90mm
TRIMMER_W   = 45                        # trimmer stud face width (X)
TRIMMER_D   = TOTAL_THICKNESS           # trimmer stud full depth (Y)
SILL_H      = 90                        # sill plate height for windows (Z)

CLEAR_W     = FRAME_W                   # clear glazing/door width inside frame
CLEAR_H     = FRAME_H                   # clear glazing/door height inside frame
RO_W        = CLEAR_W + 2 * TRIMMER_W  # rough opening width (trimmer outer faces)
HEADER_SPAN = RO_W + 2 * KING_W        # king-to-king span = total framing width
FRAME_ZONE_W = HEADER_SPAN             # X width replaced by framing (no SIP panels here)

HEADER_DEPTH = max(150, RO_W // 10)    # LVL header depth (Z); min 150mm

# OPENING_X must produce a clean left-panel width: ideally a multiple of 1200mm
# or leave a remainder panel ≥ 300mm. Align to nearest panel seam where possible.
OPENING_X = 1200   # example: one full 1200mm panel to the left, then the frame

# Vertical positions
if IS_DOOR:
    CLEAR_BASE_Z = BOTTOM_PLATE_H           # door clear void starts at bottom plate top
    TRIMMER_H    = CLEAR_H                  # trimmers height = clear door height
else:
    CLEAR_BASE_Z = BOTTOM_PLATE_H + SILL_H  # window void starts above sill
    TRIMMER_H    = SILL_H + CLEAR_H         # trimmers from bottom plate to header

HEADER_BASE_Z = BOTTOM_PLATE_H + TRIMMER_H  # Z of header underside
```

### Framing Member Summary

| Member | Width (X) | Depth (Y) | Height (Z) | Notes |
|---|---|---|---|---|
| King stud | 45mm | **TOTAL_THICKNESS** | PANEL_HEIGHT | Full height, full depth — primary load path |
| Trimmer stud | 45mm | **TOTAL_THICKNESS** | TRIMMER_H | Bears on bottom plate; carries header |
| LVL header | HEADER_SPAN | **TOTAL_THICKNESS** | HEADER_DEPTH | Spans king-to-king; min depth 150mm |
| Window sill | CLEAR_W | **TOTAL_THICKNESS** | SILL_H | Windows only; sits on trimmer tops at base of void |
| Cripple SIP | CLEAR_W | TOTAL_THICKNESS | cripple_h | Short SIP section above header to top plate |

### Panel Zone Helper and Opening Layout

Before building panels, divide the wall into solid SIP zones and framing zones. Panels must never be placed where the frame is — they stop at the king stud face.

```python
def make_panel_zone(x_start, zone_length):
    """Build SIP panels + block splines filling zone_length starting at x_start."""
    zone_parts = []
    if zone_length <= 0:
        return zone_parts
    x = x_start
    full = int(zone_length) // 1200
    rem  = int(zone_length) % 1200
    widths = [1200] * full + ([rem] if rem > 0 else [])
    for i, pw in enumerate(widths):
        f1 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT, Vector(x, 0,                         BOTTOM_PLATE_H))
        co = Part.makeBox(pw, CORE_THICKNESS, PANEL_HEIGHT, Vector(x, FACE_THICKNESS,             BOTTOM_PLATE_H))
        f2 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT, Vector(x, FACE_THICKNESS+CORE_THICKNESS, BOTTOM_PLATE_H))
        zone_parts.extend([f1, co, f2])
        x += pw
        if i < len(widths) - 1:
            sp = Part.makeBox(45, 90, PANEL_HEIGHT,
                              Vector(x - 22, (TOTAL_THICKNESS - 90) / 2, BOTTOM_PLATE_H))
            zone_parts.append(sp)
    return zone_parts

# Single opening — panel zones on each side
parts.extend(make_panel_zone(0, OPENING_X))                              # left of frame
parts.extend(make_panel_zone(OPENING_X + FRAME_ZONE_W,                  # right of frame
                             WALL_LENGTH - OPENING_X - FRAME_ZONE_W))
```

### Complete Wall-with-Opening Code

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

# === PARAMETERS ===
WALL_LENGTH     = 4800
PANEL_HEIGHT    = 2700
CORE_THICKNESS  = 150
FACE_THICKNESS  = 11
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS   # 172mm for SIP-150
BOTTOM_PLATE_H  = 90
TOP_PLATE_H     = 180

IS_DOOR    = False
FRAME_W    = 1000    # clear opening width (inside frame rebates)
FRAME_H    = 1200    # clear opening height

KING_W      = 45
KING_D      = TOTAL_THICKNESS           # FULL WALL DEPTH
TRIMMER_W   = 45
TRIMMER_D   = TOTAL_THICKNESS           # FULL WALL DEPTH
SILL_H      = 90

CLEAR_W      = FRAME_W
CLEAR_H      = FRAME_H
RO_W         = CLEAR_W + 2 * TRIMMER_W
HEADER_SPAN  = RO_W + 2 * KING_W
FRAME_ZONE_W = HEADER_SPAN
HEADER_DEPTH = max(150, RO_W // 10)

# OPENING_X = left face of left king stud (where left panels end)
# Align to a panel seam: 1200, 2400, 3600, etc.
OPENING_X    = 1200

if IS_DOOR:
    CLEAR_BASE_Z = BOTTOM_PLATE_H
    TRIMMER_H    = CLEAR_H
else:
    CLEAR_BASE_Z = BOTTOM_PLATE_H + SILL_H
    TRIMMER_H    = SILL_H + CLEAR_H

HEADER_BASE_Z = BOTTOM_PLATE_H + TRIMMER_H

doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("WallWithOpening")
parts = []

# === STEP 1: Bottom plate — full wall length (unbroken, even under opening) ===
# CORE_THICKNESS depth only — slots into the SIP foam channel, offset by FACE_THICKNESS.
bp = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, BOTTOM_PLATE_H, Vector(0, FACE_THICKNESS, 0))
parts.append(bp)

# === STEP 2: SIP panel zones — stop at king stud faces ===
def make_panel_zone(x_start, zone_length):
    zone_parts = []
    if zone_length <= 0:
        return zone_parts
    x = x_start
    full = int(zone_length) // 1200
    rem  = int(zone_length) % 1200
    widths = [1200] * full + ([rem] if rem > 0 else [])
    for i, pw in enumerate(widths):
        f1 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT, Vector(x, 0,                                  BOTTOM_PLATE_H))
        co = Part.makeBox(pw, CORE_THICKNESS, PANEL_HEIGHT, Vector(x, FACE_THICKNESS,                     BOTTOM_PLATE_H))
        f2 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT, Vector(x, FACE_THICKNESS + CORE_THICKNESS,    BOTTOM_PLATE_H))
        zone_parts.extend([f1, co, f2])
        x += pw
        if i < len(widths) - 1:
            sp = Part.makeBox(45, 90, PANEL_HEIGHT,
                              Vector(x - 22, (TOTAL_THICKNESS - 90) / 2, BOTTOM_PLATE_H))
            zone_parts.append(sp)
    return zone_parts

parts.extend(make_panel_zone(0, OPENING_X))
parts.extend(make_panel_zone(OPENING_X + FRAME_ZONE_W,
                             WALL_LENGTH - OPENING_X - FRAME_ZONE_W))

# === STEP 3: Timber buck framing (full TOTAL_THICKNESS depth throughout) ===

# King studs — full wall depth, full panel height, one each side
king_left  = Part.makeBox(KING_W, KING_D, PANEL_HEIGHT,
                           Vector(OPENING_X, 0, BOTTOM_PLATE_H))
king_right = Part.makeBox(KING_W, KING_D, PANEL_HEIGHT,
                           Vector(OPENING_X + FRAME_ZONE_W - KING_W, 0, BOTTOM_PLATE_H))
parts.extend([king_left, king_right])

# Trimmer studs — full wall depth, height from bottom plate to header underside
trimmer_left  = Part.makeBox(TRIMMER_W, TRIMMER_D, TRIMMER_H,
                              Vector(OPENING_X + KING_W, 0, BOTTOM_PLATE_H))
trimmer_right = Part.makeBox(TRIMMER_W, TRIMMER_D, TRIMMER_H,
                              Vector(OPENING_X + FRAME_ZONE_W - KING_W - TRIMMER_W, 0, BOTTOM_PLATE_H))
parts.extend([trimmer_left, trimmer_right])

# LVL header — full wall depth, spans king-to-king
header = Part.makeBox(HEADER_SPAN, TOTAL_THICKNESS, HEADER_DEPTH,
                      Vector(OPENING_X, 0, HEADER_BASE_Z))
parts.append(header)

if not IS_DOOR:
    # Window sill — full wall depth, sits on trimmers at base of clear void
    sill = Part.makeBox(CLEAR_W, TOTAL_THICKNESS, SILL_H,
                        Vector(OPENING_X + KING_W + TRIMMER_W, 0, BOTTOM_PLATE_H))
    parts.append(sill)

# Cripple SIP panels above header — short panels between header top and top plate
cripple_h = PANEL_HEIGHT - TRIMMER_H - HEADER_DEPTH
if cripple_h > 40:
    cx = OPENING_X + KING_W + TRIMMER_W
    cz = HEADER_BASE_Z + HEADER_DEPTH
    cp_f1 = Part.makeBox(CLEAR_W, FACE_THICKNESS, cripple_h, Vector(cx, 0,                                 cz))
    cp_co = Part.makeBox(CLEAR_W, CORE_THICKNESS, cripple_h, Vector(cx, FACE_THICKNESS,                    cz))
    cp_f2 = Part.makeBox(CLEAR_W, FACE_THICKNESS, cripple_h, Vector(cx, FACE_THICKNESS + CORE_THICKNESS,   cz))
    parts.extend([cp_f1, cp_co, cp_f2])

# === STEP 4: Double top plate — full wall length (unbroken) ===
# CORE_THICKNESS depth only — slots into the SIP foam channel, offset by FACE_THICKNESS.
tp = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, TOP_PLATE_H,
                  Vector(0, FACE_THICKNESS, BOTTOM_PLATE_H + PANEL_HEIGHT))
parts.append(tp)

# === STEP 5: Fuse everything into one wall solid ===
wall = parts[0]
for p in parts[1:]:
    wall = wall.fuse(p)

feature = doc.addObject("Part::Feature", "WallWithOpening")
feature.Shape = wall

# === STEP 6: Window or door unit — separate feature, not fused to wall ===
# The clear void between inner trimmer faces is the visible opening.
# Add the unit as a separate Part::Feature so it can be coloured/selected independently.

UNIT_FRAME_T = 65   # window or door frame depth (sits centred in wall depth)

if IS_DOOR:
    DOOR_LEAF_T = 44
    door_frame = Part.makeBox(CLEAR_W, UNIT_FRAME_T, CLEAR_H,
                              Vector(OPENING_X + KING_W + TRIMMER_W,
                                     (TOTAL_THICKNESS - UNIT_FRAME_T) / 2,
                                     CLEAR_BASE_Z))
    door_leaf  = Part.makeBox(CLEAR_W - 10, DOOR_LEAF_T, CLEAR_H - 15,
                              Vector(OPENING_X + KING_W + TRIMMER_W + 5,
                                     (TOTAL_THICKNESS - DOOR_LEAF_T) / 2,
                                     CLEAR_BASE_Z + 10))
    unit = door_frame.fuse(door_leaf)
else:
    GLASS_T = 28
    win_frame = Part.makeBox(CLEAR_W, UNIT_FRAME_T, CLEAR_H,
                             Vector(OPENING_X + KING_W + TRIMMER_W,
                                    (TOTAL_THICKNESS - UNIT_FRAME_T) / 2,
                                    CLEAR_BASE_Z))
    glazing   = Part.makeBox(CLEAR_W - 40, GLASS_T, CLEAR_H - 40,
                             Vector(OPENING_X + KING_W + TRIMMER_W + 20,
                                    (TOTAL_THICKNESS - GLASS_T) / 2,
                                    CLEAR_BASE_Z + 20))
    unit = win_frame.fuse(glazing)

unit_obj = doc.addObject("Part::Feature", "DoorUnit" if IS_DOOR else "WindowUnit")
unit_obj.Shape = unit

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

### Multiple Openings in One Wall

Plan all `OPENING_X` positions before building panels. Panels fill every gap between framing zones.

```python
# Define all openings as (opening_x, frame_zone_w, is_door, clear_w, clear_h)
# OPENING_X values must be sorted and non-overlapping
openings = [
    {"x": 1200, "fzw": DOOR_FRAME_ZONE_W,   "is_door": True,  "cw": 900,  "ch": 2100},
    {"x": 3000, "fzw": WIN_FRAME_ZONE_W,    "is_door": False, "cw": 1000, "ch": 1200},
]

# Build panel zones around all opening zones
solid_zones = []
prev_end = 0
for o in sorted(openings, key=lambda o: o["x"]):
    if o["x"] > prev_end:
        solid_zones.append((prev_end, o["x"] - prev_end))
    prev_end = o["x"] + o["fzw"]
if prev_end < WALL_LENGTH:
    solid_zones.append((prev_end, WALL_LENGTH - prev_end))

for x_start, zone_len in solid_zones:
    parts.extend(make_panel_zone(x_start, zone_len))

# Add framing for each opening
for o in openings:
    ox   = o["x"]
    fzw  = o["fzw"]
    # ... king studs, trimmers, header, sill per opening using ox as OPENING_X
```

### OPENING_X Alignment Guide

Choose `OPENING_X` so the panel zones on each side are buildable widths (≥ 300mm, ideally multiples of 1200mm):

| Left zone target | OPENING_X | Left zone actual | Notes |
|---|---|---|---|
| 1 full panel | 1200 | 1200mm | Clean seam |
| 2 full panels | 2400 | 2400mm | Clean seam |
| 1.5 panels | 1800 | 600mm + 1200mm | 600mm remainder — acceptable |
| Centred in 4800mm wall, 1090mm frame | 1855 | 1855mm = 1200+655 | 655mm remainder — acceptable |

---

## Pitched Roof Slopes — ALWAYS use `PitchedRoofSlope`

**Do not write manual bevel geometry.** The `PitchedRoofSlope` builder handles ridge-bevel and eave-bevel cuts internally. Manual geometry produces arithmetic errors and bypasses the constructability pipeline (which segments into 1220mm-wide buildable panels via `.construct()`).

```python
from elixifree.domains.sip import PitchedRoofSlope

# === PARAMETERS — read every value from the goal string ===
BUILDING_WIDTH = 6000         # ridge direction (X)
HALF_SPAN = 2000              # = BUILDING_DEPTH/2 (to centreline; NO +PT)
PITCH_DEG = 20
STOCK = "SIP-150"

slope = PitchedRoofSlope(
    ridge_length=BUILDING_WIDTH,
    half_span=HALF_SPAN,
    pitch_degrees=PITCH_DEG,
    stock=STOCK,
    is_south=True,            # True = south/front slope; False = north/back slope
)
result = slope.build()
result.add_to_doc("Body")
# DO NOT set feature.Placement — the assembly layer (Placer) applies pitch rotation
```

**`half_span` = `BUILDING_DEPTH/2`** (to the centreline) — never add `PANEL_THICKNESS`. The eave is placed at the wall outer face by the assembly layer, so both undersides meet on the centreline. Adding `+ PT` clashes the two slopes by `2×PT` at the ridge. See `docs/design/SIP_ROOF_STANDARDS.md`.

**North slope:** change `is_south=False`. Same geometry, different assembly rotation applied by Placer.

### Gable End Walls — use `Wall.gable()`

East and west gable end walls must have a triangular section above eave height. Use `.gable()` — do not hand-code the triangular prism:

```python
from elixifree.domains.sip import Wall

# Gable end walls BUTT between the eave walls' inner faces (corner rule, no exception) —
# span is NOT the full BUILDING_DEPTH.
GABLE_SPAN = BUILDING_DEPTH - 2 * PANEL_THICKNESS

result = (Wall(span=GABLE_SPAN, height=EAVE_HEIGHT, stock=STOCK)
    .gable(ridge_height=RIDGE_Z)
    .build())          # default local frame — do NOT call .orient("Y"); Placer sets world rotation
result.add_to_doc("Body")
# Do NOT set feature.Placement — the assembly layer (Placer) owns world position
```

See `sip_roofs.md` for flat roof, ridge board, and span-splitting details.

### 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 2–5° fall is created by a **tapered top plate** (a level-underside, sloped-top wedge) on the fall-run eave walls; the flat SIP deck rests on it and sits tilted at the fall. The deck itself is never cut into a taper, and there is NO tapered insulation. Wall-height coordination is preserved because the **walls stay level-topped** — only the plate is wedged. The membrane follows the tilted deck; it does not add its own taper.

**Fall angle:**
- **2°** — minimum design fall. Ensures drainage even after some deflection occurs in service.
- **5°** — upper end of the design range for a flat roof; beyond this the roof reads as a shallow mono-pitch, not a flat roof.

**Use a fall angle in the 2–5° range in generated models, borne entirely by the tapered top plate — never by tilting the deck or tapering the insulation.**

#### 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. The deck is ALWAYS a flat, uniform-thickness panel — it never tapers. The 2–5° fall comes from the tapered top plate underneath it (constructability layer), not from cutting the deck.
2. **Insulation board** — a flat, uniform-thickness layer on top of the deck. It never tapers and never carries the fall — the fall is structural (tapered top plate), not an insulation build-up.
3. **Waterproof membrane** — EPDM or TPO, 3mm shell on top of the insulation. It follows the tilted deck's slope as a uniform-thickness layer; it is never itself cut into a wedge.
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 |

**NON-NEGOTIABLE flat roof placement rules:**
1. The SIP roof deck bottom face sits at `Z = WALL_HEIGHT` (top of eave walls). A flat roof placed at `Z = 0` is on the ground — not the roof.
2. The roof deck spans `INNER_WIDTH × INNER_DEPTH` (between wall inner faces). Do NOT make it `BUILDING_WIDTH × BUILDING_DEPTH`.
3. `ROOF_Z = WALL_HEIGHT` always. Derive everything from this.
4. Do NOT model the roof as a single `Part.makeBox` — use the panel loop below so panel joints and splines are visible.
5. The deck and insulation are flat, uniform-thickness layers — NEVER a polygon-face wedge. The 2–5° drainage fall comes from the tapered top plate under the deck (constructability layer), not from tapering the deck or insulation.

#### 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_T    = 50            # insulation thickness (mm) — flat, uniform, NEVER tapered
MEMBRANE_T = 3             # EPDM/TPO membrane — flat, uniform, follows the deck's tilt

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

# === DRAINAGE GEOMETRY ===
# The 2–5° drainage fall is NOT modelled here. It is borne by a tapered top
# plate on the fall-run eave walls (level underside, sloped top face) that the
# constructability layer adds automatically — the SAME Fig 1.4.7 angled
# eave-plate mechanism used for pitched eaves, at a shallower angle. This
# script builds the deck, insulation, and membrane as flat, level, uniform-
# thickness layers; the tapered plate underneath is what tilts them to fall.
# Fall direction (for scupper placement only): low side at Y = PANEL_THICKNESS
# (front/south) — scuppers go on this side.
INNER_WIDTH = BUILDING_WIDTH - 2 * PANEL_THICKNESS
INNER_DEPTH = BUILDING_DEPTH - 2 * PANEL_THICKNESS
ROOF_Z = WALL_HEIGHT                         # bottom face of SIP roof panels (level; plate tilts it)

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: Insulation — flat, uniform-thickness box. NEVER a wedge/taper. ===
# The 2-5 deg fall is carried entirely by the tapered top plate under the deck
# (constructability layer); the insulation itself is always uniform thickness.
insulation = Part.makeBox(
    INNER_WIDTH, INNER_DEPTH, INSUL_T,
    Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base)
)

# === STEP 3: Membrane — flat, uniform-thickness shell on top of the insulation. ===
# NEVER a wedge — it follows the tilted deck as a uniform layer, it does not add its own taper.
membrane = Part.makeBox(
    INNER_WIDTH, INNER_DEPTH, MEMBRANE_T,
    Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_T)
)

# === 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_T - 10   # just below membrane surface (low side of the fall)

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"),
    (insulation,   "Insulation"),
    (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))
```

---

## Building Assembly — Coordinate System

**This section is the primary reference for placing all components correctly. Errors in assembly positioning are the most common source of misaligned 3D models.**

### Reference Origin

```
Origin (0, 0, 0) = the SW corner of the building footprint at Z = 0 (top of slab / floor level).
X-axis = East (along building width)
Y-axis = North (along building depth)
Z-axis = Up
```

### Slab and Footprint

The slab extends `SLAB_OVERHANG` beyond the building footprint on all four sides. The building footprint is the outer face of the wall panels.

```python
BUILDING_WIDTH = 3000     # outer face to outer face (X)
BUILDING_DEPTH = 2000     # outer face to outer face (Y)
PANEL_THICKNESS = 172     # TOTAL_THICKNESS
SLAB_OVERHANG = 200       # slab extends this far past wall face on all sides
SLAB_T = 125
SLAB_W = BUILDING_WIDTH + 2 * SLAB_OVERHANG
SLAB_D = BUILDING_DEPTH + 2 * SLAB_OVERHANG

# Slab origin: SW corner of slab at slab bottom
slab_origin = Vector(-SLAB_OVERHANG, -SLAB_OVERHANG, -SLAB_T)
slab = Part.makeBox(SLAB_W, SLAB_D, SLAB_T, slab_origin)
# Slab top surface is at Z = 0
```

### Wall Positions

For a duo-pitch gable roof with the ridge running along BUILDING_WIDTH (X):
- **EAVE walls** — south (Y=0) and north (Y=BUILDING_DEPTH): rectangular, height = EAVE_HEIGHT. The ridge runs above their centrelines.
- **GABLE END walls** — east (X=BUILDING_WIDTH) and west (X=0): triangular top. These walls are perpendicular to the ridge and must include the gable triangle section above EAVE_HEIGHT (see rule 9 and the Gable End Wall section above).

**Wall inner-face-at-origin convention:** `Wall.build()` shifts geometry so the INNER face is at local Y=0 and the OUTER face is at Y=−PT. The assembly `position` is placed at the inner face world coordinate.

```
orient("X") default:  X=[0, span],  Y=[-PT, 0],  Z=[0, height]   ← inner face at Y=0
orient("Y"):          X=[0, PT],   Y=[0, span],  Z=[0, height]   ← inner face at X=0
```

**Assembly position formula:** `pos = outer_face_world_pos + PT`

| Wall | Outer face world pos | Assembly position (inner face) |
|------|---------------------|-------------------------------|
| South (orient X) | Y=0 | pos=(0, PT, 0) |
| North (orient X) | Y=BUILDING_DEPTH | pos=(0, BUILDING_DEPTH+PT, 0) |
| West  (orient Y) | X=0 | pos=(0, PT, 0) — span starts at south inner face |
| East  (orient Y) | X=BUILDING_WIDTH | pos=(BUILDING_WIDTH-PT, PT, 0) — same Y offset |

**Corner rule (locked, no exception): eave walls run through outer-to-outer; gable end walls BUTT between the eave inner faces.** Gable end walls (east/west) span `BUILDING_DEPTH - 2*PANEL_THICKNESS` — **NOT** the full `BUILDING_DEPTH` and **NOT** `BUILDING_DEPTH - PT`. The roof does NOT bear on the gable rake (it bears on the eave walls via the angled eave top plate, Fig 1.4.7); the gable's `.gable()` triangle is non-bearing infill, so it no longer needs to reach the roof slope underside at the corners. A full-depth gable span overlaps the eave wall bodies at both corners by PT — that overlap is the bug this rule corrects, not an intentional feature. This holds regardless of which wall pair is longer.

```python
PT = PANEL_THICKNESS  # e.g. 142 for SIP-120
GABLE_SPAN = BUILDING_DEPTH - 2 * PT   # butt-adjusted — passed to Wall(span=GABLE_SPAN, ...)
```

Every wall/partition/roof-slope/foundation component script builds in its
default local frame and calls only `result.add_to_doc("Body")` — **no**
`feature.Placement`, **no** `.orient()`. The assembly Placer computes and
applies the correct world position AND rotation for every component
deterministically; see the status note at the top of this file and
`sip_walls_elixifree.md` for the current pipeline convention.

```python
from elixifree.domains.sip import Wall

result = Wall(span=BUILDING_WIDTH, height=WALL_HEIGHT, stock=STOCK).build()
result.add_to_doc("Body")
# No feature.Placement, no .orient() — the Placer owns world position and rotation
```

### Foundation Position

The Foundation builder's local origin is the **top face** — the strip geometry extends DOWN from local Z=0 to Z=−STRIP_DEPTH internally. Set `position.z = 0` to land the top face at world Z=0 (floor level):

```
position = { "x": -PT_WALL, "y": -PT_WALL, "z": 0 }
dimensions = { "length": BUILDING_WIDTH + 2*PT_WALL, "width": BUILDING_DEPTH + 2*PT_WALL, "height": STRIP_DEPTH }
```

- `position.z = 0` — MANDATORY. Using `z = -STRIP_DEPTH` DOUBLES the internal offset and buries the foundation top at world Z=−STRIP_DEPTH, leaving the walls floating above it.
- `position.x = -PT_WALL`, `position.y = -PT_WALL` — the strip extends PT_WALL beyond the building footprint on all sides so it underlies the full SIP wall thickness.

### Interior Partition Walls

Interior partitions are timber stud walls (100mm thick), not SIP panels. They always sit at `position.z = 0` (on the floor slab).

**East-west partition** (running along X, dividing north-south):
```
position.y = PT_WALL + offset_from_south_inner_face   # distance from south inner face
position.x = PT_WALL                                   # start at west inner face
span       = BUILDING_WIDTH - 2*PT_WALL               # between east and west inner faces
```

**North-south partition** (running along Y, dividing east-west):
```
position.x = PT_WALL + offset_from_west_inner_face    # distance from west inner face
position.y = PT_WALL                                   # start at south inner face
span       = BUILDING_DEPTH - 2*PT_WALL               # between south and north inner faces
```

**NEVER** place a partition at `x=0` or `y=0` — those coordinates are inside the exterior wall. The inner face of the south/north walls is at `y=PT_WALL`; the inner face of the east/west walls is at `x=PT_WALL` and `x=BUILDING_WIDTH-PT_WALL`.

Example for a building 9000×7200mm, PT=142mm, with a bedroom partition 2800mm from the south inner face:
```
position = { "x": 142, "y": 142 + 2800, "z": 0 }   → y = 2942
span = 9000 - 2*142 = 8716mm
```

### Roof Slope Positions (Gable Roof, Ridge Along X)

**The assembly layer (Placer) owns roof slope placement and rotation — you do not compute pivot offsets.** The slope is built flat with `half_span = BUILDING_DEPTH/2` (to the centreline). The Placer rotates it about its local eave origin (Y=0) by `roll = +pitch` (north slope: `yaw = 180` first) and snaps the eave to the wall **outer** face:

```
south slope:  position = (0, 0,              EAVE_HEIGHT)   # outer face y=0
north slope:  position = (0, BUILDING_DEPTH, EAVE_HEIGHT)   # outer face y=depth
```

Because `half_span = BUILDING_DEPTH/2` and the eave sits at the outer face, both slope undersides meet exactly at the centreline `y = BUILDING_DEPTH/2`. The Placer applies the eave snap deterministically, so even if the design spec emits an inner-face `y` the slopes still close at the ridge. See `docs/design/SIP_ROOF_STANDARDS.md` §2.4.

**Do NOT emit an inner-face roof position** (e.g. `y = PT_WALL + PT_PANEL·sin(pitch)`). That was the old, wrong convention — it pushes each underside `PT` past the centreline and clashes the slopes by `2×PT`.

**Ridge symmetry check:** `south_pos_y + slope_len·cos(pitch)` and `north_pos_y − slope_len·cos(pitch)` must both equal `BUILDING_DEPTH/2`.

### Quick Assembly Checklist

Before generating any component, confirm these values are consistent across all agents:

| Variable               | Where defined | Consumed by                  |
|------------------------|---------------|------------------------------|
| `PANEL_THICKNESS`      | foundation    | all walls, roof positioning  |
| `BUILDING_WIDTH`       | foundation    | all walls, roof span         |
| `BUILDING_DEPTH`       | foundation    | all walls, roof span         |
| `WALL_HEIGHT`          | any wall      | roof eave height             |
| `EAVE_HEIGHT`          | derived       | both roof slopes             |
| `RIDGE_Z`              | derived       | ridge beam                   |
| `SLAB_OVERHANG`        | foundation    | slab dimensions only         |

---

## Siding / External Cladding

Siding attaches to the exterior OSB face via battens. The foam core must never be the structural anchor for fixings.

### Layer Order (outside in)
1. Cladding (timber, fibre cement, or metal profile)
2. Battens — 50 × 25mm timber, screwed into OSB at 600mm centres
3. House wrap (Tyvek or similar) — model as a 3mm shell or omit for clarity
4. OSB exterior face (part of SIP panel)

### Batten Geometry

```python
BATTEN_W = 50
BATTEN_D = 25
BATTEN_SPACING = 600   # centre to centre
WALL_HEIGHT_TOTAL = BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H

batten_count = WALL_LENGTH // BATTEN_SPACING
battens = []
for i in range(batten_count + 1):
    x = min(i * BATTEN_SPACING, WALL_LENGTH - BATTEN_W)
    b = Part.makeBox(BATTEN_W, BATTEN_D, WALL_HEIGHT_TOTAL,
                     Vector(x, -BATTEN_D, 0))   # sits proud of exterior OSB face
    battens.append(b)
```

### Cladding Types

**Timber weatherboard** — 150–200mm wide boards, 25–30mm thick, horizontal lapped:
```python
BOARD_W = 175
BOARD_T = 25
LAP = 40   # overlap
exposed = BOARD_W - LAP

board_count = int(math.ceil(WALL_HEIGHT_TOTAL / exposed))
for i in range(board_count):
    z = i * exposed
    board = Part.makeBox(WALL_LENGTH, BOARD_T, BOARD_W,
                         Vector(0, -(BATTEN_D + BOARD_T), z))
    # add to parts
```

**Fibre cement planks** — 200–300mm wide, 8–10mm thick, horizontal direct-fix or on battens. Same pattern as timber weatherboard with BOARD_T = 9.

**Metal profile (corrugated or standing seam)** — 0.7mm TCT steel, modelled as a flat shell for structural purposes:
```python
cladding_shell = Part.makeBox(WALL_LENGTH, 1, WALL_HEIGHT_TOTAL,
                              Vector(0, -(BATTEN_D + 1), 0))
```

---

## Foundations

See `sip_foundations.md` for full foundation code. Summary:

- **Slab-on-grade** — no ElixiFree builder; write raw `Part` geometry (slab + edge beams + anchor bolts). `SLAB_OVERHANG` ≥ 200mm; slab top = Z=0.
- **Strip foundation** — use `Foundation(..., type="strip_foundation").build()`. Do NOT write raw `Part.makeBox` fuse code.
- **Screw pile** — no ElixiFree builder; write raw `Part` geometry.

---

## Ventilation

SIP panels are extremely airtight — there is negligible incidental air leakage through the structure. Every SIP building **requires deliberately designed ventilation**. Without it, CO₂ accumulates, moisture condenses inside the panel structure, and the building becomes unhealthy. Ventilation is not optional; it is a building system component that must be modelled.

### Strategy by Building Size

| Floor area        | Strategy                          | Minimum provisions                                      |
|-------------------|-----------------------------------|---------------------------------------------------------|
| < 15 m² (garden room, studio) | **Passive through-wall vents**  | 2 × 100mm wall vents (inlet low + exhaust high, opposite walls) + trickle vents in window frames |
| 15–50 m² (cabin, annexe)      | **Passive + extract fan**       | 4 × 100mm vents, 1 × bathroom/kitchen extract fan      |
| > 50 m² (house)               | **MVHR unit + ductwork**        | Mechanical ventilation with heat recovery, supply + extract to each room |

### Vent Sizing Calculation

```python
import math

# === BUILDING DIMENSIONS ===
FLOOR_AREA_M2   = (BUILDING_WIDTH / 1000) * (BUILDING_DEPTH / 1000)
CEILING_H_M     = (BOTTOM_PLATE_H + PANEL_HEIGHT) / 1000   # floor-to-underside-of-structure
ROOM_VOLUME_M3  = FLOOR_AREA_M2 * CEILING_H_M

# Minimum air change rate:
#   0.5 ACH — habitable room with openable windows
#   1.0 ACH — habitable room with no opening windows (sealed glazing)
ACH = 0.5
MIN_FLOW_M3H = ROOM_VOLUME_M3 * ACH           # m³/hr
MIN_FLOW_LS  = MIN_FLOW_M3H / 3.6            # litres/sec

# Minimum vent free area (assuming ~1.5 m/s air velocity at vent)
VENT_VELOCITY  = 1.5   # m/s
AREA_NEEDED_M2 = (MIN_FLOW_M3H / 3600) / VENT_VELOCITY
AREA_NEEDED_MM2 = AREA_NEEDED_M2 * 1e6      # mm²

# Standard round duct free areas
AREA_100MM = math.pi * 50**2    # ~7,854 mm²
AREA_125MM = math.pi * 62.5**2  # ~12,272 mm²
AREA_150MM = math.pi * 75**2    # ~17,671 mm²

# Number of 100mm vents required (rounded up, minimum 2: one inlet + one exhaust)
vents_needed = max(2, math.ceil(AREA_NEEDED_MM2 / AREA_100MM))
# Example: 3×2m room → 6 m², vol 16.2 m³, flow 8.1 m³/hr → 1.5 mm² → 1 duct → use 2 (inlet + exhaust)
```

### Through-Wall Vent (Passive — small buildings)

**Placement rules:**
- Inlet: low on the wall (200–300mm above finished floor), on the prevailing wind side or the wall most exposed to fresh air
- Exhaust: high on the opposite wall (200mm below the top plate / ceiling level)
- Never place inlet and exhaust on the same wall — air will short-circuit and not ventilate the space
- Minimum 2m horizontal separation between any inlet and exhaust
- Keep inlets away from external corners (dead-air zones)
- All duct penetrations go through the full OSB + foam + OSB wall depth; seal around duct with compressible acoustic foam

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

# === PARAMETERS ===
DUCT_RADIUS   = 50        # 100mm diameter duct
COWL_W        = 150       # external weather cowl width
COWL_H        = 150       # external weather cowl height
COWL_D        = 60        # external weather cowl depth (protrudes from wall face)
GRILLE_D      = 10        # internal grille plate depth

# Inlet vent — low on south wall, centred
INLET_X = BUILDING_WIDTH / 2
INLET_Z = BOTTOM_PLATE_H + 250    # 250mm above finished floor

# Cut duct void through full wall thickness (south wall, Y = 0 to TOTAL_THICKNESS)
inlet_void = Part.makeCylinder(
    DUCT_RADIUS, TOTAL_THICKNESS,
    Vector(INLET_X, 0, INLET_Z),
    Vector(0, 1, 0)            # direction: through wall in +Y
)
south_wall = south_wall.cut(inlet_void)

# External weather cowl (louvred hood, sits proud of exterior OSB face)
ext_cowl = Part.makeBox(
    COWL_W, COWL_D, COWL_H,
    Vector(INLET_X - COWL_W / 2, -COWL_D, INLET_Z - COWL_H / 2)
)

# Internal grille plate (flush with interior OSB face)
int_grille = Part.makeBox(
    COWL_W, GRILLE_D, COWL_H,
    Vector(INLET_X - COWL_W / 2, TOTAL_THICKNESS, INLET_Z - COWL_H / 2)
)

# Exhaust vent — high on north wall, centred
EXHAUST_X = BUILDING_WIDTH / 2
EXHAUST_Z  = BOTTOM_PLATE_H + PANEL_HEIGHT - 250   # 250mm below top of panels

exhaust_void = Part.makeCylinder(
    DUCT_RADIUS, TOTAL_THICKNESS,
    Vector(EXHAUST_X, BUILDING_DEPTH - TOTAL_THICKNESS, EXHAUST_Z),
    Vector(0, 1, 0)
)
north_wall = north_wall.cut(exhaust_void)

ext_exhaust_cowl = Part.makeBox(
    COWL_W, COWL_D, COWL_H,
    Vector(EXHAUST_X - COWL_W / 2,
           BUILDING_DEPTH,
           EXHAUST_Z - COWL_H / 2)
)
int_exhaust_grille = Part.makeBox(
    COWL_W, GRILLE_D, COWL_H,
    Vector(EXHAUST_X - COWL_W / 2,
           BUILDING_DEPTH - TOTAL_THICKNESS - GRILLE_D,
           EXHAUST_Z - COWL_H / 2)
)

# Add to document
for shape, name in [
    (ext_cowl,           "VentInletCowl"),
    (int_grille,         "VentInletGrille"),
    (ext_exhaust_cowl,   "VentExhaustCowl"),
    (int_exhaust_grille, "VentExhaustGrille"),
]:
    obj = doc.addObject("Part::Feature", name)
    obj.Shape = shape
```

### MVHR Unit (Mechanical Ventilation with Heat Recovery — buildings > 50 m²)

The MVHR unit is a box mounted on an internal wall or in a utility cupboard. It has four duct connections: fresh-air intake and stale-air exhaust penetrating one external wall, plus supply and extract ducts that distribute air through the building internally (omit internal ductwork at concept stage; model the wall penetrations only).

```python
# === MVHR PARAMETERS ===
MVHR_W = 600      # unit width
MVHR_H = 400      # unit height
MVHR_D = 300      # unit depth (projects from wall into room)
DUCT_R  = 80      # 160mm diameter supply/extract ducts

# MVHR unit body — mounted on north interior wall, 300mm above floor
mvhr_unit = Part.makeBox(
    MVHR_W, MVHR_D, MVHR_H,
    Vector(PANEL_THICKNESS + 100,
           BUILDING_DEPTH - PANEL_THICKNESS - MVHR_D,
           BOTTOM_PLATE_H + 300)
)

# Fresh-air intake duct — penetrates north wall (low port)
intake_void = Part.makeCylinder(
    DUCT_R, PANEL_THICKNESS,
    Vector(PANEL_THICKNESS + 100 + MVHR_W * 0.25,
           BUILDING_DEPTH - PANEL_THICKNESS,
           BOTTOM_PLATE_H + 300 + DUCT_R),
    Vector(0, 1, 0)
)
north_wall = north_wall.cut(intake_void)

# Stale-air exhaust duct — penetrates north wall (high port, beside intake)
exhaust_void = Part.makeCylinder(
    DUCT_R, PANEL_THICKNESS,
    Vector(PANEL_THICKNESS + 100 + MVHR_W * 0.75,
           BUILDING_DEPTH - PANEL_THICKNESS,
           BOTTOM_PLATE_H + 300 + MVHR_H - DUCT_R * 2),
    Vector(0, 1, 0)
)
north_wall = north_wall.cut(exhaust_void)

# External duct terminals (short stubs proud of exterior wall face)
intake_terminal = Part.makeCylinder(
    DUCT_R, 150,
    Vector(PANEL_THICKNESS + 100 + MVHR_W * 0.25,
           BUILDING_DEPTH,
           BOTTOM_PLATE_H + 300 + DUCT_R),
    Vector(0, 1, 0)
)
exhaust_terminal = Part.makeCylinder(
    DUCT_R, 150,
    Vector(PANEL_THICKNESS + 100 + MVHR_W * 0.75,
           BUILDING_DEPTH,
           BOTTOM_PLATE_H + 300 + MVHR_H - DUCT_R * 2),
    Vector(0, 1, 0)
)

for shape, name in [
    (mvhr_unit,        "MVHRUnit"),
    (intake_terminal,  "MVHRIntakeTerminal"),
    (exhaust_terminal, "MVHRExhaustTerminal"),
]:
    obj = doc.addObject("Part::Feature", name)
    obj.Shape = shape
```

### Roof Vent Terminal (Flat Roof — duct routed vertically)

When ductwork rises vertically through the roof rather than through a wall (e.g., MVHR exhaust on a flat roof), cut the penetration through the roof stack and add a weathered terminal cap.

```python
# === ROOF VENT TERMINAL ===
TERMINAL_R   = 80     # 160mm duct
TERMINAL_H   = 350    # stub height above roof membrane
CAP_R        = 130    # cowl cap radius (wider than duct)
CAP_H        = 50

# Position: above MVHR unit, clear of parapet shadow
TERM_X = PANEL_THICKNESS + 100 + MVHR_W * 0.75
TERM_Y = BUILDING_DEPTH - PANEL_THICKNESS - MVHR_D / 2

# Cut penetration through insulation and membrane
penetration_depth = INSUL_T + MEMBRANE_T   # through insulation + membrane (both flat, uniform)
terminal_void = Part.makeCylinder(
    TERMINAL_R, penetration_depth,
    Vector(TERM_X, TERM_Y, insul_z_base)
)
insulation = insulation.cut(terminal_void)
membrane   = membrane.cut(terminal_void)

# Duct stub above roof
duct_stub = Part.makeCylinder(
    TERMINAL_R, TERMINAL_H,
    Vector(TERM_X, TERM_Y, insul_z_base + penetration_depth)
)

# Cowl cap (mushroom-head rain guard)
cowl_cap = Part.makeCylinder(
    CAP_R, CAP_H,
    Vector(TERM_X, TERM_Y, insul_z_base + penetration_depth + TERMINAL_H)
)

for shape, name in [
    (duct_stub, "RoofVentDuct"),
    (cowl_cap,  "RoofVentCowl"),
]:
    obj = doc.addObject("Part::Feature", name)
    obj.Shape = shape
```

### Ventilation Component in the Building Hierarchy

For small buildings (passive vents): include vent penetrations and cowls as part of the **wall component** that they penetrate.

For buildings with MVHR: add a dedicated **ventilation component** that depends on the wall components it penetrates.

```
walls (south, north, east, west)
  └── ventilation   (depends on: south_wall, north_wall — cuts penetrations and adds cowls/unit)
```

**Ventilation component provides:**
- `inlet_position` — {x, y, z} of duct centreline at inlet wall interior face
- `exhaust_position` — {x, y, z} of duct centreline at exhaust wall interior face

---

## MANDATORY: Part Code Embossing

Every component script that receives a `PART CODE:` instruction in its task MUST include the `_emboss_part_code` helper and call it on the final shape **before** `obj.Shape = <shape>`. The complete function template is provided verbatim in the task text — copy it exactly, do not rewrite or shorten it.

Key rules:
- Declare `PART_CODE = "<code>"` and `PRINT_SCALE = 50` in the `# === PARAMETERS ===` block.
- Replace `<final_shape>` in the two call sites at the bottom of the template with the actual variable name that holds your completed geometry (e.g. `result`, `base_plate`, `panel`).
- The function is wrapped in `try/except` — if embossing fails for any geometry reason, it returns the original shape silently. Generation will still succeed.
- Do not modify the matrix math, depth formulas, or wire-sorting logic.

## MANDATORY: Foundation Component

**Every SIP building manifest MUST include a `foundation` component.** No exceptions. The foundation is always generated — even when the user hasn't mentioned the foundation in the prompt. It is the root of the dependency tree and provides the floor level, building footprint dimensions, and panel_thickness to every wall agent.

**NON-NEGOTIABLE — agent id MUST be exactly `foundation`:**

The foundation agent id is always the literal string `foundation`. No other name is acceptable. The validator, dependency resolver, and all downstream agents depend on this exact string.

❌ FORBIDDEN agent ids — do not use any of these:
- `concrete_slab` — wrong: this is a material description, not the role id
- `slab` — wrong: too generic
- `strip_foundation` — wrong: describes the type, not the role
- `foundation_slab` — wrong: reversed
- `base_plate` / `base_plate_south` / `base_plate_north` — wrong: base plates are sole plates, not the foundation
- `foundation_front` / `foundation_south` / `foundation_strip_north` — wrong: never split the foundation into sides

✅ CORRECT: one agent, id = `foundation`, priority = 1, no dependencies.

**Rules:**
- Agent id: `foundation` (exactly this string — see forbidden list above)
- Priority: 1 (highest — no dependencies)
- Dependencies: none
- Foundation type defaults to `slab_on_grade` unless the design spec explicitly states otherwise
- Use `BUILDING_WIDTH` and `BUILDING_DEPTH` from the design spec `overall_dimensions` for the footprint (the slab extends SLAB_OVERHANG = 200mm beyond this on all sides)
- The foundation is modelled as a single flat piece: slab field + perimeter edge beams as one fused solid
- `assembly_placement.position` = `{x: -200, y: -200, z: -125}` for a 125mm slab with 200mm overhang (slab bottom is -125mm, SW corner of slab is at -200, -200)
- `bounding_box` = `{x: BUILDING_WIDTH + 400, y: BUILDING_DEPTH + 400, z: 125}` (slab field only; edge beam extends deeper)

**Goal string template:**
```
Create slab-on-grade foundation, BUILDINGWIDTHx1200mm slab overhang 200mm all sides, 125mm slab thickness, 600mm edge beam depth, 300mm edge beam width. Single fused solid. Z=0 is slab top.
```
(Replace BUILDINGWIDTH and BUILDINGDEPTH with actual mm values from the spec.)

**If the spec mentions a strip foundation:** use a SINGLE agent with id `foundation`, priority 1. The strip is a flat perimeter ring — one model, one component. Do NOT create four separate side agents. See the Strip Foundation section above for exact geometry and Python code.

**Wrong vs correct — strip foundation:**
```
❌ WRONG (split into sides):
  { "id": "foundation_front", "priority": 1, ... }
  { "id": "foundation_back",  "priority": 1, ... }
  { "id": "foundation_left",  "priority": 1, ... }
  { "id": "foundation_right", "priority": 1, ... }

✅ CORRECT (single perimeter ring):
  { "id": "foundation", "priority": 1, "dependencies": [], ... }
```

**If the spec mentions a screw pile foundation**, use the screw pile catalog entry; use agent id `foundation`.

## Component Agent Breakdown

For a rectangular SIP building, use this component hierarchy. Each component declares interfaces that the next tier depends on.

**Pitched roof — slab-on-grade foundation:**
```
foundation
  ├── south_wall     (depends on: foundation.floor_level, foundation.building_width)
  │     ├── sole_plate_anchor_bolts_south   (depends on: south_wall.plate_length)
  │     ├── top_plate_screws_south          (depends on: south_wall.plate_length, south_wall.wall_top)
  │     └── roof_hurricane_ties_south_eave  (depends on: south_wall.wall_top, south_wall.plate_length)
  ├── north_wall     (depends on: foundation.floor_level, foundation.building_width)
  │     ├── sole_plate_anchor_bolts_north
  │     ├── top_plate_screws_north
  │     └── roof_hurricane_ties_north_eave
  ├── west_wall      (depends on: foundation.floor_level, foundation.building_depth, south_wall.panel_thickness)
  │     ├── sole_plate_anchor_bolts_west
  │     └── top_plate_screws_west
  └── east_wall      (depends on: foundation.floor_level, foundation.building_depth, south_wall.panel_thickness)
        ├── sole_plate_anchor_bolts_east
        ├── top_plate_screws_east
        ├── roof_south_slope  (depends on: south_wall.wall_top, east_wall.wall_top, west_wall.wall_top)
        ├── roof_north_slope  (depends on: north_wall.wall_top, east_wall.wall_top, west_wall.wall_top)
        └── ridge_beam        (depends on: roof_south_slope.ridge_z, foundation.building_depth)
```

**Pitched roof — strip foundation (single foundation agent):**
```
foundation     (priority 1, no deps — single flat perimeter strip ring, Z=0 is top/bearing face)
  ├── south_wall     (depends on: foundation.floor_level, foundation.building_width)
  │     ├── sole_plate_anchor_bolts_south
  │     ├── top_plate_screws_south
  │     └── roof_hurricane_ties_south_eave
  ├── north_wall     (depends on: foundation.floor_level, foundation.building_width)
  │     ├── sole_plate_anchor_bolts_north
  │     ├── top_plate_screws_north
  │     └── roof_hurricane_ties_north_eave
  ├── west_wall      (depends on: foundation.floor_level, foundation.building_depth, south_wall.panel_thickness)
  │     ├── sole_plate_anchor_bolts_west
  │     └── top_plate_screws_west
  └── east_wall      (depends on: foundation.floor_level, foundation.building_depth, south_wall.panel_thickness)
        ├── sole_plate_anchor_bolts_east
        ├── top_plate_screws_east
        ├── roof_south_slope
        ├── roof_north_slope
        └── ridge_beam
```

**Flat roof — slab-on-grade foundation:**
```
foundation
  ├── south_wall     (depends on: foundation.floor_level, foundation.building_width)
  │     ├── sole_plate_anchor_bolts_south
  │     ├── top_plate_screws_south
  │     └── roof_hurricane_ties_south_eave
  ├── north_wall     (depends on: foundation.floor_level, foundation.building_width)
  │     ├── sole_plate_anchor_bolts_north
  │     ├── top_plate_screws_north
  │     └── roof_hurricane_ties_north_eave
  ├── west_wall      (depends on: foundation.floor_level, foundation.building_depth, south_wall.panel_thickness)
  │     ├── sole_plate_anchor_bolts_west
  │     └── top_plate_screws_west
  └── east_wall      (depends on: foundation.floor_level, foundation.building_depth, south_wall.panel_thickness)
        ├── sole_plate_anchor_bolts_east
        ├── top_plate_screws_east
        ├── flat_roof         (depends on: south_wall.wall_top, building_width, building_depth)
        │     └── parapets    (depends on: flat_roof.roof_z, flat_roof.roof_thickness)
        └── ventilation       (depends on: south_wall or north_wall wall geometry)
```

**Add ventilation for all buildings; corner post bases when posts are present:**
```
walls
  └── ventilation    (depends on: the wall(s) it penetrates)

foundation (if corner posts present)
  ├── corner_post_base_sw    (depends on: foundation, south_wall.panel_thickness)
  ├── corner_post_base_se
  ├── corner_post_base_nw
  └── corner_post_base_ne
```

### Interface Definitions

**foundation** (slab-on-grade) provides:
- `floor_level` = 0 (Z coordinate of slab top — always 0 by convention)
- `building_width` — outer face to outer face (X)
- `building_depth` — outer face to outer face (Y)
- `panel_thickness` — TOTAL_THICKNESS of the chosen SIP spec
- `slab_overhang` — how far slab extends past wall faces

**strip foundation** — single `foundation` agent (flat perimeter ring). Provides `floor_level = 0` (top face is Z=0). Walls depend on `foundation.floor_level` and derive dimensions from the spec.

**wall** provides:
- `wall_top` — Z of top of double top plate = WALL_HEIGHT
- `wall_height` = BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H
- `wall_length` — outer dimension of this wall face
- `panel_thickness` — passthrough

**roof slope** (pitched) provides:
- `ridge_z` — Z coordinate of ridge panel top edge
- `slope_length` — along-slope panel run length
- `pitch_deg`

**flat_roof** provides:
- `roof_z` = WALL_HEIGHT (Z of roof panel bottom)
- `roof_top_z` = WALL_HEIGHT + ROOF_THICKNESS + INSUL_T + MEMBRANE_T (flat, level layers — the 2–5° fall itself is applied by the tapered top plate under the deck, not reflected in this level-frame formula)
- `parapet_z` = `roof_z` (parapets start at wall top, same Z as roof panels)

**ventilation** provides:
- `inlet_position` — {x, y, z} of inlet duct centreline at interior wall face
- `exhaust_position` — {x, y, z} of exhaust duct centreline at interior wall face

---

## Building Prompt Patterns

When a user asks for a SIP building, decompose the prompt into these questions before generating components:

1. **Floor plan** — overall dimensions (length × width in mm)?
2. **Wall panel spec** — SIP-150 (default), SIP-200, etc.?
3. **Wall height** — floor-to-ceiling (typically 2400–2700mm); add 270mm for plates
4. **Roof type** — flat (modern default), mono-pitch, or duo-pitch gable? Pitch angle if pitched (default 20°)?
5. **Foundation type** — slab (default), strip+timber floor, or screw pile?
6. **Openings** — how many windows/doors, on which walls, approximate sizes?
7. **Siding** — timber weatherboard (default), fibre cement, or metal?
8. **Ventilation** — always include. < 15m²: passive through-wall vents; ≥ 50m²: MVHR unit.

**Roof type guidance:**
- "modern", "contemporary", "minimal", "flat roof", "studio", "garden room" → use **flat roof** with parapets
- "cabin", "traditional", "cottage", "gable", "pitched" → use **duo-pitch**
- "lean-to", "mono-pitch", "shed roof" → use **mono-pitch**

For a project brief like "a 6×9m SIP cabin with gable roof":
- Foundation: concrete slab, BUILDING_WIDTH=6000, BUILDING_DEPTH=9000, SLAB_OVERHANG=200
- 4 walls: gable end walls 6000mm full width; side walls 9000 - 2×172 = 8656mm
- Roof: duo-pitch, 20°, HALF_SPAN=4500mm, ridge_height=1638mm
- No siding component unless asked
- Openings added to specific walls when described

For a project brief like "a 3×4m modern SIP garden room":
- Foundation: concrete slab, BUILDING_WIDTH=3000, BUILDING_DEPTH=4000, SLAB_OVERHANG=200
- 4 walls: SIP-150, PANEL_HEIGHT=2700, WALL_HEIGHT=2970
- Roof: flat, SIP-200 deck, ROOF_THICKNESS=222, INSUL_T=50 (flat, uniform — fall borne by tapered top plate), PARAPET_H=300
- Windows and door on south wall

### MANDATORY: Design Spec `"roof"` Section

Every SIP design spec **MUST** include a top-level `"roof"` object with `roof_type_key` and `roof_structure_key`. Without it the profile validator will warn on every generation. Use the table below:

| Roof type | `roof_type_key` | `roof_structure_key` |
|-----------|-----------------|----------------------|
| Flat / warm deck | `"flat"` | `"roof_structure/sip_flat_warm_roof"` |
| Mono-pitch | `"mono_pitch"` | `"roof_structure/sip_mono_pitch_panels"` |
| Duo-pitch gable | `"duo_pitch"` | `"roof_structure/sip_simple_gable_panels"` |

Example for a duo-pitch gable:

```json
"roof": {
  "roof_type_key": "duo_pitch",
  "roof_structure_key": "roof_structure/sip_simple_gable_panels",
  "pitch_degrees": 20,
  "ridge_direction": "east_west"
}
```

This field is required in the spec JSON even when full roof component details appear separately in the `components` array.

---

## MANDATORY: Fasteners

Every SIP building manifest **MUST** include fastener agents. Fasteners are structural — they are not optional decoration. The following connections require mechanical fixing in every SIP model.

### Required Fastener Connections

| Connection | Fastener | Library Key | Spacing |
|---|---|---|---|
| Sole plate → foundation slab | M12 anchor bolt + washer + nut | `hardware/anchors/m12_anchor_bolt_200mm` | 600mm centres, ≤150mm from each end |
| Top/sole plate → panel OSB | Structural screw #10 × 150mm | `hardware/screws/structural_screw_10g_x_150mm` | 150mm centres |
| Spline → panel edge | Structural screw #10 × 150mm | `hardware/screws/structural_screw_10g_x_150mm` | 300mm centres |
| Corner post → sole plate | Post base 100mm | `hardware/framing/post_base_100mm` | 1 per post |
| Roof panel → wall top plate | Hurricane tie | `hardware/framing/hurricane_tie` | 600mm centres along each eave |

### Fastener Agent Strategy

**Arrays of fasteners** (anchor bolts along a plate, screws along a panel) are modelled as **`source_kind: "generated"`** agents. The agent generates an array of fastener primitives in FreeCAD, positioned at the correct spacing along the plate length.

**Single instances** (one post base per corner post) are **`source_kind: "library"`** agents referencing the library part directly.

### Manifest Naming Convention

```
sole_plate_anchor_bolts_south     — anchor bolts for south wall sole plate
sole_plate_anchor_bolts_north
top_plate_screws_south            — screws fixing south wall top plate to panels
top_plate_screws_north
roof_hurricane_ties_south_eave    — hurricane ties at south eave
roof_hurricane_ties_north_eave
corner_post_base_sw               — post base at SW corner (if corner posts present)
corner_post_base_se
corner_post_base_nw
corner_post_base_ne
```

### FreeCAD Code Pattern — Anchor Bolt Array

```python
import FreeCAD
import Part
from FreeCAD import Vector

# === PARAMETERS ===
PLATE_LENGTH = 3756        # from parent interface
PLATE_Y_OFFSET = 11        # FACE_THICKNESS — plate is offset from wall face
CORE_THICKNESS = 100       # CORE_THICKNESS of SIP
BOLT_DIA = 12
BOLT_SHAFT_LENGTH = 200
BOLT_EMBEDMENT = 150       # below slab top (Z=0 is slab top)
WASHER_DIA = 30
HEAD_H = 7.5
WASHER_T = 3.0
END_OFFSET = 150           # first bolt distance from plate end
SPACING = 600

import math
n_bolts = max(2, math.ceil((PLATE_LENGTH - 2 * END_OFFSET) / SPACING) + 1)
actual_spacing = (PLATE_LENGTH - 2 * END_OFFSET) / (n_bolts - 1) if n_bolts > 1 else 0

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

# Bolt centred in plate depth: Y = PLATE_Y_OFFSET + CORE_THICKNESS/2
bolt_y = PLATE_Y_OFFSET + CORE_THICKNESS / 2
# Z origin for shaft: from -BOLT_EMBEDMENT to top of washer+nut
bolt_z_base = -BOLT_EMBEDMENT

solids = []
for i in range(n_bolts):
    bx = END_OFFSET + i * actual_spacing

    shaft = Part.makeCylinder(BOLT_DIA / 2, BOLT_SHAFT_LENGTH, Vector(bx, bolt_y, bolt_z_base))

    # Hex head above shaft
    import math as _math
    haf = 19 / 2 / _math.cos(_math.pi / 6)
    hex_pts = [Vector(bx + haf * _math.cos(_math.radians(60*k)), bolt_y + haf * _math.sin(_math.radians(60*k)),
                      bolt_z_base + BOLT_SHAFT_LENGTH) for k in range(6)]
    hex_pts.append(hex_pts[0])
    hw = Part.makePolygon(hex_pts)
    hf = Part.Face(Part.Wire(hw.Edges))
    head = hf.extrude(Vector(0, 0, HEAD_H))

    washer = Part.makeCylinder(WASHER_DIA / 2, WASHER_T,
                               Vector(bx, bolt_y, bolt_z_base + BOLT_SHAFT_LENGTH))
    washer_hole = Part.makeCylinder(BOLT_DIA / 2 + 0.5, WASHER_T,
                                    Vector(bx, bolt_y, bolt_z_base + BOLT_SHAFT_LENGTH))
    washer = washer.cut(washer_hole)

    solids.extend([shaft, head, washer])

result = solids[0]
for s in solids[1:]:
    result = result.fuse(s)

obj = doc.addObject("Part::Feature", "AnchorBolts")
obj.Shape = result
doc.recompute()
```

### FreeCAD Code Pattern — Hurricane Tie Array

```python
import FreeCAD
import Part
from FreeCAD import Vector

# === PARAMETERS ===
EAVE_LENGTH = 3756         # length of eave edge
SPACING = 600
STRAP_W = 38
STRAP_T = 2.5
VERT_LEG = 90              # up the roof panel edge
HORIZ_LEG = 50             # across the top plate
HOLE_DIA = 5

import math
n_ties = max(2, math.ceil(EAVE_LENGTH / SPACING))
actual_spacing = EAVE_LENGTH / n_ties

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

solids = []
for i in range(n_ties):
    tx = i * actual_spacing + actual_spacing / 2 - STRAP_W / 2
    # Vertical leg
    vert = Part.makeBox(STRAP_T, STRAP_W, VERT_LEG, Vector(tx, 0, 0))
    # Horizontal leg (extends in Y across top plate)
    horiz = Part.makeBox(HORIZ_LEG, STRAP_W, STRAP_T, Vector(tx + STRAP_T, 0, 0))
    tie = vert.fuse(horiz)
    solids.append(tie)

result = solids[0]
for s in solids[1:]:
    result = result.fuse(s)

obj = doc.addObject("Part::Feature", "HurricaneTies")
obj.Shape = result
doc.recompute()
```

### Fastener Agent Dependencies

Fastener agents depend on their parent structural component:

```
foundation
  ├── south_wall
  │     ├── sole_plate_anchor_bolts_south   (depends on: south_wall.plate_length, foundation)
  │     ├── top_plate_screws_south          (depends on: south_wall.plate_length, south_wall.wall_top)
  │     └── roof_hurricane_ties_south_eave  (depends on: south_wall.wall_top, south_wall.plate_length)
  └── ... (same for each wall)
```

### Assembly Placement for Fastener Agents

Anchor bolt arrays: `assembly_placement.position` = `{x: wall_x, y: wall_y, z: 0}` — same XY offset as the wall they belong to; Z=0 is slab top and the bolts embed downward.

Hurricane ties: `assembly_placement.position` = eave corner of the wall they tie to; Z = wall_top Z.

Post bases: `assembly_placement.position` = corner post position; Z = sole plate top face.

---

## Common Mistakes to Avoid

1. **Never use face references** — always position by coordinate offset
2. **Slab must be larger than building footprint** — SLAB_W = BUILDING_WIDTH + 2 × SLAB_OVERHANG (minimum 200mm each side); a slab equal to the footprint will leave walls hanging at the edge
3. **Panel foam must stay above ground** — check DPC_T + SILL_PLATE_H ≥ 150mm above finished ground
4. **Anchor bolts go into the concrete** — model with negative Z (embedment below slab top)
5. **Spline depth must match foam depth** — block spline D = CORE_THICKNESS (not TOTAL_THICKNESS)
6. **Roof panels must have ridge bevel cuts** — without bevel, the panels collide at the ridge and cannot mate flush; ridge_bevel_x = TOTAL_THICKNESS × tan(pitch_rad)
7. **Roof rotation pivot is the eave edge interior face** — rotate around (x=0, y=TOTAL_THICKNESS, z=EAVE_HEIGHT), not around (0, 0, 0)
8. **Double top plate = 180mm** — two 45×90 plates stacked; wall_top = BOTTOM_PLATE_H + PANEL_HEIGHT + 180
9. **Window/door openings require all four framing members** — king studs + trimmer studs + LVL header + sill plate (windows); missing king studs means the opening has no structural connection to the wall panels above
10. **Side walls fit between gable walls** — side wall length = BUILDING_DEPTH − 2 × PANEL_THICKNESS, offset from wall exterior by PANEL_THICKNESS
11. **Flat roof panels span the inner width, not the outer width** — roof panels sit between wall interior faces; X span = BUILDING_WIDTH − 2 × PANEL_THICKNESS, starting at X = PANEL_THICKNESS
12. **Flat roofs use SIP-200 or thicker** — SIP-150 is insufficient for a flat cold roof; specify ROOF_CORE ≥ 200mm
13. **Never tilt or taper flat roof SIP panels, and never taper the insulation to create drainage fall** — build the deck and insulation as flat, level, uniform-thickness layers; the 2–5° fall is borne structurally by a tapered top plate on the fall-run eave walls (constructability layer adds it automatically), the same Fig 1.4.7 eave-plate mechanism used for pitched eaves
14. **Parapets start at wall top Z, not at roof top Z** — PARAPET_Z = WALL_HEIGHT (same as ROOF_Z), so the parapet and roof panels begin at the same level and the parapet extends above the roof surface
15. **Insulation is always a flat box, never a wedge** — the drainage fall comes from the tapered top plate under the deck, not from tapering the insulation; the insulation stays uniform thickness
16. **Scuppers must be cut through the parapet** — flat roofs with parapets drain through rectangular holes in the parapet panel, not through the deck; the outlet cylinder in the deck alone does nothing if the parapet retains water
17. **Every wall component generates its own bottom plate** — the foundation component does not generate bottom plates. The `Wall` builder includes the bottom plate automatically. If writing manual geometry, start with `bp = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, BOTTOM_PLATE_H, Vector(0, FACE_THICKNESS, 0))` at Z=0 — CORE_THICKNESS depth, offset by FACE_THICKNESS.
18. **Walls with openings must include the glazing/door unit** — after cutting the void and adding framing, add a box representing the window frame + glazing or door leaf; without this the opening renders as an empty hole
19. **Every SIP building must include a ventilation component** — SIP construction is airtight; omitting ventilation means the building has no fresh air path; always generate at minimum an inlet and exhaust vent even for small garden rooms
20. **Inlet and exhaust vents must be on opposite walls** — placing both on the same wall causes short-circuit airflow; no fresh air reaches the interior; inlet on south/windward wall, exhaust on north/leeward wall
21. **Vent ducts penetrate the full wall thickness** — cut the cylinder void through all three layers (OSB + EPS + OSB); a duct that only penetrates the OSB face is blocked by the foam core and delivers no airflow
22. **Scale vent count to floor area** — use `vents_needed = max(2, math.ceil(AREA_NEEDED_MM2 / AREA_100MM))`; a single 100mm vent is sufficient for rooms under ~10m², two are standard minimum; larger buildings need proportionally more or an MVHR unit
23. **King studs and trimmer studs must be TOTAL_THICKNESS deep** — `KING_D = TOTAL_THICKNESS`, not 90mm. A 90mm-deep king stud centred in the foam leaves a gap on each side; the SIP OSB faces have nothing to bear against and the frame is not tied through the wall depth
24. **Never build a continuous wall then cut a void for the opening** — plan the opening zone before building panels and use `make_panel_zone` to stop panels at the king stud face. Cutting a void after fusing removes material that was never there structurally; framing members added afterwards float inside the wall solid rather than replacing it
25. **OPENING_X is the left face of the left king stud** — not the left edge of the clear void. The clear void starts at `OPENING_X + KING_W + TRIMMER_W`. Using the void edge as OPENING_X shifts the entire frame inward and places panels inside the framing zone
26. **Bottom plate and top plate run the full wall length unbroken** — including under and over the opening. The frame bears on the bottom plate; the double top plate continues over the header distributing the load. A gap in the plate at the opening leaves the frame floating
27. **Window sill is the full wall depth** — `SILL_D = TOTAL_THICKNESS`, same as every other framing member. A narrower sill leaves the window unit unsupported on the inner or outer face and cannot be properly flashed or sealed
28. **Never use `INSUL_MIN` or `FALL_RATIO` — these no longer exist.** Insulation thickness is a single flat value (`INSUL_T`); the drainage fall is not an insulation parameter at all — it is set by the tapered top plate's fall angle (2–5°), applied by the constructability layer
29. **Never encode a "1:N" fall ratio in the roof script** — the fall is a 2–5° angle borne by the tapered top plate, not a deck or insulation slope computed in the component script. `FALL_HEIGHT`, `FALL_RATIO`, and similar formulas do not belong in flat-roof component code
30. **The membrane must be a flat box, never a wedge** — model it as a uniform-thickness layer on top of the insulation surface. It follows the deck's tilt (from the tapered top plate below) as a rigid body; it does not add its own taper
31. **Insulation must be a flat box, never a polygon extrude wedge** — `Part.makeBox(..., INSUL_T)` is correct; a trapezoid/wedge profile is the OLD (removed) pattern. The insulation is uniform thickness; the fall lives entirely in the tapered top plate beneath the deck
32. **Fastener agents must be included in every manifest** — sole plate anchor bolts, roof hurricane ties, and corner post bases are all mandatory. A manifest that lists only structural SIP components is incomplete; the assembly will be missing all mechanical connections.
33. **Anchor bolt Y position is plate centreline, not wall face** — bolt_y = FACE_THICKNESS + CORE_THICKNESS/2. Placing bolts at Y=0 or Y=TOTAL_THICKNESS/2 misses the plate and embeds into the OSB skin or slab overhang.
34. **Fastener arrays use `source_kind: "generated"`, not `"library"`** — arrays of bolts or screws along a plate are generated agents that produce parametric arrays; a library agent produces only a single instance.
v0.0.985