Personalize Elixihub behavior for this browser. Changes save automatically.
Workspace Preferences
Control how the component browser, assembly viewer, and keyboard editing behave.
Stored in browser
Help & Guide
ElixiCAD uses AI agents to generate parametric 3D CAD models from plain-English descriptions.
What is ElixiCAD?
ElixiCAD lets you describe a 3D object in natural language and have AI design and model every individual component, then assemble them into a complete CAD file you can download or send to a 3D printer.
Creating a project — step by step
1
Click New Project
Give your project a name and a brief description. Include key dimensions, materials, and intended use.
2
Choose an AI provider and generation profile
The settings gear (⌥1) lets you choose between Claude, Gemini, and others. Balanced is a good starting point.
3
Wait for the design brief and component decomposition
The system writes a design brief and spec, then creates an AI agent for each component. This takes 30–90 seconds.
4
Review and approve components
When generation completes you will see a 3D preview. Click Approve to accept, or add feedback to request changes.
5
Create the assembly
Navigate to the Assembly tab and click Create Assembly once all components are approved.
6
Download or 3D print
Use Download for a tar archive of all STEP, STL, and FCStd files.
Tips for better results
✓Include dimensions.
Vague descriptions produce inconsistent results. Specify mm or cm.
✓Describe how parts connect.
The assembly agent uses this to position parts correctly.
✓Keep projects focused.
5–12 components works well. Very large assemblies take longer and have more placement errors.
✓Use feedback to iterate.
Describe specific issues rather than asking for a full regeneration.
Navigation (⌥ = Option/Alt key)
⌥ (hold)
Show shortcut numbers on the right nav
⌥ + 1
Open Settings
⌥ + 2
Open Skills
⌥ + 3
Open Standard Parts
⌥ + 4
Go to Test Lab
⌥ + 5
Open Help
⌥ + 6
Sign In / Sign Out
Global
Ctrl+S
Open Settings
Escape
Close any open modal
Ctrl+M
Toggle measurement mode (3D viewer)
3D Viewer
Ctrl+1
Plan view
Ctrl+2
Elevation view
Ctrl+3
End view
Ctrl+4
Isometric view
Component Board
← → ↑ ↓
Navigate component cards
Enter
Open the focused component
Assembly View
Ctrl+H
Toggle visibility of selected component
Ctrl+L
Toggle the component legend
Delete
Back to project
Assembly Edit Mode
← →
Move component along X axis
↑ ↓
Move component along Y axis
Ctrl+↑ / Ctrl+↓
Move component along Z axis
X / Y / Z
Rotate around that axis
Standard Parts
Reusable catalog components available for project generation.
Fasteners
Anchoring & Mounting
Hinges & Motion
Latching & Locking
Structural & Joining
Woodworking
Metal Fabrication
Other
Loading preview…
Skills
Reusable project guidance that can be attached to new projects.
# 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.
---
## 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. **Flat roofs MUST have a visible drainage fall of 1:20 and scupper outlets.**
Use `FALL_HEIGHT = INNER_DEPTH / 20` and `INSUL_MIN = 50`. Tapered insulation MUST be a polygon-face wedge (not a flat box), and the membrane on top MUST also be a matching wedge so the finished roof surface reads as sloped. Scupper openings MUST be cut through the parapet at the low side. A flat roof with no scuppers behind a parapet will flood. Never use `INSUL_MIN = 18` or `FALL_RATIO = 40` — these are code floors, not design values.
5. **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.
6. **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.
7. **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.**
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=EAVE_HEIGHT, AND (b) a triangular prism from Z=EAVE_HEIGHT to Z=RIDGE_Z whose cross-section in the YZ plane is a triangle with vertices (0, EAVE_HEIGHT), (BUILDING_DEPTH/2, RIDGE_Z), (BUILDING_DEPTH, EAVE_HEIGHT). A gable wall that stops at EAVE_HEIGHT leaves an open triangular gap — unenclosed, weather-exposed, and structurally wrong.
10. **Duo-pitch roof panels MUST use the correct placement formula — south and north slopes are NOT mirror copies of each other.**
Panel local frame: X = BUILDING_WIDTH (ridge), Y = slope_length (0 = eave, slope_length = ridge end), Z = TOTAL_THICKNESS (0 = interior face, TT = exterior face). Ridge bevel cut in the YZ plane at Y = slope_length. South slope: `Rotation(Vector(1,0,0), PITCH_DEG)`, base `Vector(0, PANEL_THICKNESS, EAVE_HEIGHT)`. North slope: `Rotation(Vector(1,0,0), 180 - PITCH_DEG)`, base `Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, EAVE_HEIGHT)`. Using `Rotation(X, -PITCH_DEG)` for north, or copying south and rotating differently, produces panels that float away from the ridge or face the wrong direction.
---
## 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.
---
## Panel FreeCAD Code Pattern
Model each SIP panel as a 3-layer fused solid: two OSB faces + EPS foam core.
```python
import FreeCAD, Part, math
from FreeCAD import Vector
# === PARAMETERS ===
PANEL_WIDTH = 1200 # mm
PANEL_HEIGHT = 2700 # mm
CORE_THICKNESS = 150 # mm (SIP-150)
FACE_THICKNESS = 11 # mm OSB each side
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS # 172mm
# === BUILD PANEL ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("SIP_Panel")
face1 = Part.makeBox(PANEL_WIDTH, FACE_THICKNESS, PANEL_HEIGHT,
Vector(0, 0, 0))
core = Part.makeBox(PANEL_WIDTH, CORE_THICKNESS, PANEL_HEIGHT,
Vector(0, FACE_THICKNESS, 0))
face2 = Part.makeBox(PANEL_WIDTH, FACE_THICKNESS, PANEL_HEIGHT,
Vector(0, FACE_THICKNESS + CORE_THICKNESS, 0))
panel = face1.fuse(core).fuse(face2)
obj = doc.addObject("Part::Feature", "SIP_Panel")
obj.Shape = panel
doc.recompute()
```
---
## 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
**Every wall is a sequence of individual 1200mm SIP panels joined by block splines, sitting on a pressure-treated bottom plate. Never model a wall as a single box.**
```
┌─────────────────────────────────────┐ ← double top plate (2× 45×90mm) Z = BOTTOM_PLATE_H + PANEL_HEIGHT
│ Panel │Sp│ Panel │Sp│ Panel │ ← SIP panels Z = BOTTOM_PLATE_H (= 90)
└─────────────────────────────────────┘ ← PT bottom plate (45×90mm) Z = 0
══════════════════════════════════════ ← slab top / floor level Z = 0
```
**Bottom plate (mandatory — generated by the wall component, not the foundation):**
45 × 90mm pressure-treated timber, full wall length, sits at Z=0 on the slab DPC. The SIP panels start at Z=90 on top of this plate. If the wall code does not include a bottom plate at Z=0, the panels will be floating directly on concrete — this is wrong.
**Top plate:** Double 45 × 90mm timber, full wall length, nailed through OSB face into foam.
**Wall height** = BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H = 90 + panel_height + 180mm
### Panel Count Calculation
Always calculate how many full 1200mm panels fit and what remainder is needed:
```python
full_panels = WALL_LENGTH // 1200 # number of full-width panels
remainder = WALL_LENGTH % 1200 # width of last custom panel (0 = exact fit)
panel_count = full_panels + (1 if remainder > 0 else 0)
panel_widths = [1200] * full_panels + ([remainder] if remainder > 0 else [])
```
Common wall lengths:
| Wall length | Full panels | Remainder panel |
|-------------|-------------|-----------------|
| 2400mm | 2 | none |
| 3000mm | 2 | 600mm |
| 3600mm | 3 | none |
| 4800mm | 4 | none |
| 6000mm | 5 | none |
| 9000mm | 7 | 600mm |
### Wall Assembly Code
```python
import FreeCAD, Part
from FreeCAD import Vector
# === WALL PARAMETERS ===
WALL_LENGTH = 3000 # example: 3m wall = 2 full panels + 1×600mm remainder
PANEL_HEIGHT = 2700
CORE_THICKNESS = 150
FACE_THICKNESS = 11
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS # 172mm for SIP-150
BOTTOM_PLATE_H = 90 # PT timber bottom plate — MANDATORY
TOP_PLATE_H = 180 # double top plate (2× 45mm)
SPLINE_W = 45
SPLINE_D = 90
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Wall")
# --- Panel widths (calculated, not hardcoded) ---
full_panels = WALL_LENGTH // 1200
remainder = WALL_LENGTH % 1200
panel_widths = [1200] * full_panels + ([remainder] if remainder > 0 else [])
# === STEP 1: Bottom plate (MANDATORY — wall sits on this, not directly on slab) ===
# Depth = CORE_THICKNESS (not TOTAL_THICKNESS) — plate slots into the foam groove.
# OSB skins (FACE_THICKNESS each) overhang on both faces; offset plate by FACE_THICKNESS.
bp = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, BOTTOM_PLATE_H, Vector(0, FACE_THICKNESS, 0))
parts = [bp]
# === STEP 2: SIP panels and block splines — one panel per entry in panel_widths ===
x = 0
for i, pw in enumerate(panel_widths):
# Three-layer SIP panel
face1 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT,
Vector(x, 0, BOTTOM_PLATE_H))
core = Part.makeBox(pw, CORE_THICKNESS, PANEL_HEIGHT,
Vector(x, FACE_THICKNESS, BOTTOM_PLATE_H))
face2 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT,
Vector(x, FACE_THICKNESS + CORE_THICKNESS, BOTTOM_PLATE_H))
parts.extend([face1, core, face2])
x += pw
# Block spline at joint between this panel and the next
if i < len(panel_widths) - 1:
sp = Part.makeBox(SPLINE_W, SPLINE_D, PANEL_HEIGHT,
Vector(x - SPLINE_W / 2,
(TOTAL_THICKNESS - SPLINE_D) / 2,
BOTTOM_PLATE_H))
parts.append(sp)
# === STEP 3: Double top plate ===
# Same rule as bottom plate: CORE_THICKNESS depth, 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)
wall = parts[0]
for p in parts[1:]:
wall = wall.fuse(p)
feature = doc.addObject("Part::Feature", "Wall")
feature.Shape = wall
doc.recompute()
```
---
## 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 |
---
## Roof Panels
SIP roof panels are the same composite construction as wall panels but oriented at the roof pitch. The slope is defined by the pitch angle.
### Ridge Bevel — CRITICAL for Watertight Fit
Without a bevel cut at the ridge, the two slope panels overrun the centreline and cannot mate flush. Always cut the bevel in the flat panel before applying the placement rotation.
**Panel local frame (all pitched roof code uses this):**
- X = 0 to BUILDING_WIDTH (along ridge)
- Y = 0 to slope_length (along slope, 0 = eave end, slope_length = ridge end)
- Z = 0 to TOTAL_THICKNESS (0 = interior/bottom face, TT = exterior/top face)
**Rule:** cut a triangular prism from the ridge end (Y = slope_length) in the YZ plane, extruded along X. After rotating the flat panel to pitch angle, this cut face becomes plumb (vertical), and the two slope faces mate flush at the apex.
```
Flat panel cross-section in YZ plane (before rotation):
←── slope_length ──────────────── bevel ──┐
──────────────────────────────────────────│ Z = 0 (interior face)
│
──────────────────────────────╱───────────│ Z = TT (exterior face)
↑
ridge end: exterior face is cut back by bevel_y = TT × tan(pitch)
interior face stays at Y = slope_length
```
**Formulae:**
- `bevel_y = TOTAL_THICKNESS * tan(PITCH_DEG)` — ridge bevel setback at exterior face
- `eave_bevel_y = TOTAL_THICKNESS / tan(PITCH_DEG)` — eave bevel setback at interior face
```python
import math, Part
from FreeCAD import Vector, Placement, Rotation
PITCH_RAD = math.radians(PITCH_DEG)
# Panel builder: X = ridge_length, Y = slope_length, Z = thickness
def make_slope_panel(ridge_len, slope_len, face_t, core_t):
f1 = Part.makeBox(ridge_len, slope_len, face_t)
c = Part.makeBox(ridge_len, slope_len, core_t, Vector(0, 0, face_t))
f2 = Part.makeBox(ridge_len, slope_len, face_t, Vector(0, 0, face_t + core_t))
return f1.fuse(c).fuse(f2)
# Ridge bevel: cut triangle at Y = slope_length end (YZ plane, extruded in X)
# Triangle: (sl - bevel_y, TT), (sl, TT), (sl, 0) — removes exterior overhang
def cut_ridge_bevel_slope(panel, sl, bevel_y, ridge_len, tt):
pts = [Vector(0, sl - bevel_y, tt),
Vector(0, sl, tt),
Vector(0, sl, 0),
Vector(0, sl - bevel_y, tt)]
return panel.cut(Part.Face(Part.makePolygon(pts)).extrude(Vector(ridge_len, 0, 0)))
# Eave bevel: cut triangle at Y = 0 end so panel bears on angled-cut top plate
# Triangle: (0, 0), (eave_bv, 0), (0, TT) — removes interior corner at eave
def cut_eave_bevel_slope(panel, eave_bv, ridge_len, tt):
pts = [Vector(0, 0, 0),
Vector(0, eave_bv, 0),
Vector(0, 0, tt),
Vector(0, 0, 0)]
return panel.cut(Part.Face(Part.makePolygon(pts)).extrude(Vector(ridge_len, 0, 0)))
```
### Mono-Pitch Roof
Single slope from low eave wall to high wall (or ridge). Uses the same panel local frame as duo-pitch: X = ridge length (BUILDING_WIDTH), Y = along slope, Z = thickness.
```python
# === MONO-PITCH PARAMETERS ===
BUILDING_WIDTH = 6000 # ridge/eave length (X direction)
BUILDING_DEPTH = 4000 # horizontal span from eave to high wall (Y direction)
PITCH_DEG = 20
PANEL_WIDTH_ROOF = 1200
CORE_THICKNESS = 150
FACE_THICKNESS = 11
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS
EAVE_HEIGHT = 2960 # Z of top of low eave wall top plate
PITCH_RAD = math.radians(PITCH_DEG)
slope_length = BUILDING_DEPTH / math.cos(PITCH_RAD) # along-slope distance
rise = BUILDING_DEPTH * math.tan(PITCH_RAD) # height gained across span
bevel_y = TOTAL_THICKNESS * math.tan(PITCH_RAD) # ridge bevel setback
eave_bevel_y = TOTAL_THICKNESS / math.tan(PITCH_RAD) # eave bevel setback
n_panels = math.ceil(slope_length / PANEL_WIDTH_ROOF)
roof_parts = []
y_pos = 0.0
for i in range(n_panels):
pw = min(PANEL_WIDTH_ROOF, slope_length - y_pos)
p = make_slope_panel(BUILDING_WIDTH, pw, FACE_THICKNESS, CORE_THICKNESS)
p.translate(Vector(0, y_pos, 0))
if i == n_panels - 1: # ridge/high-wall end bevel
p = cut_ridge_bevel_slope(p, pw, bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
if i == 0: # eave end bevel
p = cut_eave_bevel_slope(p, eave_bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
roof_parts.append(p)
y_pos += pw
roof = roof_parts[0]
for p in roof_parts[1:]:
roof = roof.fuse(p)
# Rotation by +PITCH_DEG around X tilts the Y (slope) axis upward into +Z.
# Base places the eave bottom corner (local 0,0,0) at the inner face of the low wall top plate.
rot = Rotation(Vector(1, 0, 0), PITCH_DEG)
pl = Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot)
roof_obj = doc.addObject("Part::Feature", "Roof")
roof_obj.Shape = roof
roof_obj.Placement = pl
```
### Duo-Pitch (Gable) Roof
Two slopes meeting at a central ridge beam. Each slope is built in the SAME local frame and gets a ridge bevel so the faces mate flush at the apex. South and north placements differ — do not copy one from the other with a simple sign flip.
**Orientation convention:** ridge runs along BUILDING_WIDTH (X), slope spans BUILDING_DEPTH (Y). South slope eave at Y ≈ PANEL_THICKNESS; north slope eave at Y ≈ BUILDING_DEPTH − PANEL_THICKNESS; ridge at Y = BUILDING_DEPTH/2.
**Panel local frame:** X = 0…BUILDING_WIDTH (ridge direction), Y = 0…slope_length (slope, 0 = eave, slope_length = ridge end), Z = 0…TOTAL_THICKNESS (0 = interior/bottom face, TT = exterior/top face).
```python
# === DUO-PITCH PARAMETERS ===
HALF_SPAN = BUILDING_DEPTH / 2 # horizontal half-span (eave to ridge centreline)
slope_length = HALF_SPAN / math.cos(PITCH_RAD)
ridge_height = HALF_SPAN * math.tan(PITCH_RAD)
RIDGE_Z = EAVE_HEIGHT + ridge_height
# Ridge bevel: cut wedge from ridge end (Y = slope_length) so the face becomes
# vertical after the slope is rotated to pitch. bevel_y = TT * tan(P)
bevel_y = TOTAL_THICKNESS * math.tan(PITCH_RAD)
# Eave bevel: cut wedge from eave end (Y = 0) so the panel bears cleanly on the
# angled-cut top plate. eave_bevel_y = TT / tan(P) = TT * cot(P)
eave_bevel_y = TOTAL_THICKNESS / math.tan(PITCH_RAD)
# --- Panel builder (X = ridge, Y = slope, Z = thickness) ---
def make_slope_panel(bw, sl, face_t, core_t):
f1 = Part.makeBox(bw, sl, face_t)
c = Part.makeBox(bw, sl, core_t, Vector(0, 0, face_t))
f2 = Part.makeBox(bw, sl, face_t, Vector(0, 0, face_t + core_t))
return f1.fuse(c).fuse(f2)
# Ridge bevel cut — removes triangle at Y = slope_length end (in YZ plane, extruded along X)
def cut_ridge_bevel_slope(panel, sl, bv_y, bw, tt):
pts = [Vector(0, sl - bv_y, tt),
Vector(0, sl, tt),
Vector(0, sl, 0),
Vector(0, sl - bv_y, tt)]
face = Part.Face(Part.makePolygon(pts))
return panel.cut(face.extrude(Vector(bw, 0, 0)))
# Eave bevel cut — removes triangle at Y = 0 end
def cut_eave_bevel_slope(panel, eb_y, bw, tt):
pts = [Vector(0, 0, 0),
Vector(0, eb_y, 0),
Vector(0, 0, tt),
Vector(0, 0, 0)]
face = Part.Face(Part.makePolygon(pts))
return panel.cut(face.extrude(Vector(bw, 0, 0)))
# --- Build south slope ---
n_panels = math.ceil(slope_length / PANEL_WIDTH_ROOF)
south_parts = []
y_pos = 0.0
for i in range(n_panels):
pw = min(PANEL_WIDTH_ROOF, slope_length - y_pos)
p = make_slope_panel(BUILDING_WIDTH, pw, FACE_THICKNESS, CORE_THICKNESS)
p.translate(Vector(0, y_pos, 0))
if i == n_panels - 1:
p = cut_ridge_bevel_slope(p, pw, bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
if i == 0:
p = cut_eave_bevel_slope(p, eave_bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
south_parts.append(p)
y_pos += pw
south_roof = south_parts[0]
for p in south_parts[1:]:
south_roof = south_roof.fuse(p)
# South placement: +PITCH_DEG around X tilts the Y (slope) axis up toward +Z.
# Base puts the eave bottom corner (local 0,0,0) at the inner face of south wall top.
rot_s = Rotation(Vector(1, 0, 0), PITCH_DEG)
pl_s = Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot_s)
south_obj = doc.addObject("Part::Feature", "RoofSouthSlope")
south_obj.Shape = south_roof
south_obj.Placement = pl_s
# --- North slope (same panel geometry, different placement) ---
north_roof = south_roof.copy()
# North placement: (180 - PITCH_DEG) around X maps local +Y to world -Y direction,
# so Y=0 (eave end) stays at the north wall inner face and Y=slope_length (ridge end)
# reaches world Y = BUILDING_DEPTH/2. Do NOT use -PITCH_DEG — that sends the ridge
# further away from centre, not toward it.
rot_n = Rotation(Vector(1, 0, 0), 180 - PITCH_DEG)
pl_n = Placement(Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, EAVE_HEIGHT), rot_n)
north_obj = doc.addObject("Part::Feature", "RoofNorthSlope")
north_obj.Shape = north_roof
north_obj.Placement = pl_n
# --- Ridge beam (LVL or glulam, vertical, centred at ridge) ---
RIDGE_BEAM_D = max(200, int(HALF_SPAN / 10)) # depth (Z), min 200mm
RIDGE_BEAM_W = 90 # width (Y)
ridge = Part.makeBox(
BUILDING_WIDTH,
RIDGE_BEAM_W,
RIDGE_BEAM_D,
Vector(0, BUILDING_DEPTH / 2 - RIDGE_BEAM_W / 2, RIDGE_Z)
)
ridge_obj = doc.addObject("Part::Feature", "RidgeBeam")
ridge_obj.Shape = ridge
```
**Key placement sanity check (substitute real numbers to verify):**
- South eave bottom at world (x, PANEL_THICKNESS, EAVE_HEIGHT) ✓
- South ridge bottom at world (x, PANEL_THICKNESS + HALF_SPAN, RIDGE_Z) ≈ (x, BUILDING_DEPTH/2, RIDGE_Z) ✓
- North eave bottom at world (x, BUILDING_DEPTH − PANEL_THICKNESS, EAVE_HEIGHT) ✓
- North ridge bottom at world (x, BUILDING_DEPTH/2 + PANEL_THICKNESS − small, RIDGE_Z) ≈ (x, BUILDING_DEPTH/2, RIDGE_Z) ✓
### Gable End Wall — Triangular Section
For a duo-pitch roof (ridge along X), the **east and west walls** are gable end walls. They must include the full triangular gable section above EAVE_HEIGHT. Build each gable wall component as two fused solids:
1. **Rectangular base** — standard SIP panel loop from Z=0 to Z=EAVE_HEIGHT (full BUILDING_DEPTH length)
2. **Triangular gable prism** — above EAVE_HEIGHT, triangular cross-section in the YZ plane, extruded PANEL_THICKNESS in X
```python
# Triangular gable section for east or west gable end wall
# Cross-section (YZ plane) triangle:
# (Y=0, Z=EAVE_HEIGHT) ← south corner (eave height)
# (Y=BUILDING_DEPTH/2, Z=RIDGE_Z) ← apex (ridge height)
# (Y=BUILDING_DEPTH, Z=EAVE_HEIGHT) ← north corner (eave height)
gable_pts = [
Vector(0, 0, EAVE_HEIGHT),
Vector(0, BUILDING_DEPTH / 2, RIDGE_Z),
Vector(0, BUILDING_DEPTH, EAVE_HEIGHT),
Vector(0, 0, EAVE_HEIGHT), # close
]
gable_wire = Part.makePolygon(gable_pts)
gable_face = Part.Face(gable_wire)
gable_prism = gable_face.extrude(Vector(PANEL_THICKNESS, 0, 0))
# West gable wall (exterior face at X = 0):
# The rectangular base wall is already built in the wall loop at X=0.
# Fuse the gable prism on top — it sits at X=0 to X=PANEL_THICKNESS, Y=0 to BUILDING_DEPTH.
west_gable = wall_solid.fuse(gable_prism) # wall_solid = rectangular section
# East gable wall (exterior face at X = BUILDING_WIDTH):
gable_prism_east = gable_prism.copy()
gable_prism_east.translate(Vector(BUILDING_WIDTH - PANEL_THICKNESS, 0, 0))
east_gable = wall_solid_east.fuse(gable_prism_east)
```
**The angled top face of the gable prism naturally matches the roof pitch** — no additional cut needed. The face slope from (0, EAVE_HEIGHT) to (BUILDING_DEPTH/2, RIDGE_Z) is `rise / run = ridge_height / HALF_SPAN = tan(PITCH_DEG)` ✓
### Flat Roof
A flat roof uses horizontal SIP panels resting directly on the wall top plates. Use SIP-200 or SIP-250 for flat roofs — the additional insulation compensates for the reduced stack effect and improves thermal performance at the cold deck.
**Drainage fall is not optional — a truly flat surface ponds water and will fail.** The slope is created by tapered insulation on top of the horizontal SIP deck; the SIP deck itself stays level. Do not tilt the structural panels — it makes wall height coordination impossible.
**Fall rates:**
- **1:40 (25mm/m)** — absolute code minimum (BS 8217). Produces ~40mm height difference over a 1.6m inner span; barely visible in a 3D model and leaves no margin for deflection or construction tolerance.
- **1:20 (50mm/m)** — **design target.** Use this for all models. Produces ~80mm height difference over a 1.6m inner span, clearly visible, and ensures drainage even after some deflection occurs in service.
**Always use 1:20 in generated models.** The formula is `FALL_HEIGHT = INNER_DEPTH / 20`.
#### Flat Roof Elements
1. **SIP roof panels** — horizontal, spanning the shorter building dimension. Multiple 1200mm-wide panels joined by splines, same panel-loop pattern as walls.
2. **Tapered insulation board** — modelled as a **wedge** (thick at high side, thin at low side) using a polygon-face extrude. Use `FALL_RATIO = 20` (1:20 design fall). Minimum thickness at the low point: **50mm** (absolute code floor is 18mm but 50mm gives visible slope and prevents ponding in construction tolerances).
3. **Waterproof membrane** — EPDM or TPO, 3mm shell on top of the insulation.
4. **Parapet walls** — short SIP-100 panels extending above roof level on all four sides. The interior face of the parapet is lined with the membrane upstand.
5. **Scupper outlets through the parapet** — rectangular holes cut through the parapet panel at the low side of the fall. These are the only way water can leave a parapeted roof. Without scuppers the roof will flood.
6. **Overflow scuppers** — a second set of scupper holes 50mm higher than the primary, as a backup if the primary blocks.
7. **Downpipe stubs** — short cylinder representing the external downpipe at each primary scupper.
8. **Flat roof bearer (optional)** — for spans > 3000mm, a mid-span LVL bearer under the panels reduces deflection.
#### Parapet vs. Open Eave
| Detail | Parapet | Open Eave / Fascia |
|--------------|----------------------------------|--------------------------------|
| Look | Modern, clean — walls continue past roof | Reveals roof edge from below |
| Water | Contained; internal drainage | Drains freely at edge |
| Model | Extend wall panels up by PARAPET_H above roof | Fascia board at panel end |
| Default | **Use this for modern flat roofs** | Use for agricultural/industrial |
#### Flat Roof Code
```python
import FreeCAD, Part, math
from FreeCAD import Vector
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("FlatRoof")
# === PARAMETERS ===
BUILDING_WIDTH = 3000 # outer face to outer face (X)
BUILDING_DEPTH = 2000 # outer face to outer face (Y)
WALL_HEIGHT = 2970 # BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H
PANEL_THICKNESS = 172 # wall SIP total thickness
ROOF_CORE = 200 # SIP-200 for flat roofs
ROOF_FACE = 11
ROOF_THICKNESS = ROOF_CORE + 2 * ROOF_FACE # 222mm
ROOF_PANEL_W = 1200
INSUL_MIN = 50 # insulation at low point (mm) — 50mm design minimum; code floor is 18mm
FALL_RATIO = 20 # 1:20 design fall — use this, not 1:40 (code minimum only)
MEMBRANE_T = 3 # EPDM/TPO membrane
PARAPET_H = 300 # parapet height above ROOF_Z
PARAPET_T = 122 # SIP-100 parapet panels
COPING_T = 3 # aluminium coping
# === DRAINAGE GEOMETRY ===
# Fall direction: high side at Y = BUILDING_DEPTH - PANEL_THICKNESS (back/north)
# low side at Y = PANEL_THICKNESS (front/south) — scuppers here
INNER_WIDTH = BUILDING_WIDTH - 2 * PANEL_THICKNESS
INNER_DEPTH = BUILDING_DEPTH - 2 * PANEL_THICKNESS
FALL_HEIGHT = INNER_DEPTH / FALL_RATIO # 1:20 design fall — e.g. 80mm over 1600mm inner depth
INSUL_HIGH = INSUL_MIN + FALL_HEIGHT # thickness at high side
# Confirm the slope is visible: FALL_HEIGHT should be >> 0
# e.g. 3000×2000 building: INNER_DEPTH≈1656mm → FALL_HEIGHT≈83mm, INSUL range 50→133mm
ROOF_Z = WALL_HEIGHT # bottom face of SIP roof panels
insul_z_base = ROOF_Z + ROOF_THICKNESS # top of roof deck = base of insulation
# === STEP 1: SIP Roof Deck (multiple panels — same loop as walls) ===
full_panels = INNER_DEPTH // ROOF_PANEL_W
remainder = INNER_DEPTH % ROOF_PANEL_W
panel_widths_roof = [ROOF_PANEL_W] * full_panels + ([remainder] if remainder > 0 else [])
roof_parts = []
y = PANEL_THICKNESS
for i, pw in enumerate(panel_widths_roof):
f1 = Part.makeBox(INNER_WIDTH, pw, ROOF_FACE,
Vector(PANEL_THICKNESS, y, ROOF_Z))
co = Part.makeBox(INNER_WIDTH, pw, ROOF_CORE,
Vector(PANEL_THICKNESS, y, ROOF_Z + ROOF_FACE))
f2 = Part.makeBox(INNER_WIDTH, pw, ROOF_FACE,
Vector(PANEL_THICKNESS, y, ROOF_Z + ROOF_FACE + ROOF_CORE))
roof_parts.append(f1.fuse(co).fuse(f2))
if i < len(panel_widths_roof) - 1:
sp = Part.makeBox(INNER_WIDTH, 45, 90,
Vector(PANEL_THICKNESS, y + pw - 22.5,
ROOF_Z + (ROOF_THICKNESS - 90) / 2))
roof_parts.append(sp)
y += pw
roof_deck = roof_parts[0]
for p in roof_parts[1:]:
roof_deck = roof_deck.fuse(p)
# === STEP 2: Tapered insulation — WEDGE, not a flat box ===
# Profile in Y-Z plane, extruded in X.
# Low side (front, Y = PANEL_THICKNESS): insul_z_base + INSUL_MIN
# High side (back, Y = BUILDING_DEPTH - PANEL_THICKNESS): insul_z_base + INSUL_HIGH
insul_pts = [
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base + INSUL_HIGH),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base),
]
insul_wire = Part.makePolygon(insul_pts)
insul_face = Part.Face(insul_wire)
tapered_insul = insul_face.extrude(Vector(INNER_WIDTH, 0, 0))
# === STEP 3: Membrane — wedge following insulation slope, same profile in Y-Z plane ===
# Must be a wedge, not a flat box, so the finished roof surface reads as sloped in the model.
mem_pts = [
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base + INSUL_HIGH),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base + INSUL_HIGH + MEMBRANE_T),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN + MEMBRANE_T),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN),
]
mem_wire = Part.makePolygon(mem_pts)
mem_face = Part.Face(mem_wire)
membrane = mem_face.extrude(Vector(INNER_WIDTH, 0, 0))
# === STEP 4: Parapet walls (four sides) ===
PARAPET_Z = ROOF_Z # parapets start at same Z as roof panels (wall top)
PARAPET_TOP_Z = PARAPET_Z + PARAPET_H
par_s = Part.makeBox(BUILDING_WIDTH, PARAPET_T, PARAPET_H,
Vector(0, 0, PARAPET_Z))
par_n = Part.makeBox(BUILDING_WIDTH, PARAPET_T, PARAPET_H,
Vector(0, BUILDING_DEPTH - PARAPET_T, PARAPET_Z))
par_w = Part.makeBox(PARAPET_T, BUILDING_DEPTH - 2 * PARAPET_T, PARAPET_H,
Vector(0, PARAPET_T, PARAPET_Z))
par_e = Part.makeBox(PARAPET_T, BUILDING_DEPTH - 2 * PARAPET_T, PARAPET_H,
Vector(BUILDING_WIDTH - PARAPET_T, PARAPET_T, PARAPET_Z))
# === STEP 5: Scuppers through south parapet at low side — MANDATORY ===
# Primary scupper: centred on south parapet, at membrane level
SCUPPER_W = 100 # opening width
SCUPPER_H = 75 # opening height
SCUPPER_Z = insul_z_base + INSUL_MIN - 10 # just below low-point membrane surface
scupper_primary = Part.makeBox(
SCUPPER_W, PARAPET_T, SCUPPER_H,
Vector(BUILDING_WIDTH / 2 - SCUPPER_W / 2, 0, SCUPPER_Z)
)
par_s = par_s.cut(scupper_primary)
# Overflow scupper: offset to one side, 50mm higher (backup if primary blocks)
scupper_overflow = Part.makeBox(
SCUPPER_W, PARAPET_T, SCUPPER_H,
Vector(BUILDING_WIDTH / 4 - SCUPPER_W / 2, 0, SCUPPER_Z + 50)
)
par_s = par_s.cut(scupper_overflow)
# === STEP 6: Downpipe stub at primary scupper (exterior face) ===
DOWNPIPE_R = 50 # 100mm dia downpipe
downpipe = Part.makeCylinder(
DOWNPIPE_R, 400,
Vector(BUILDING_WIDTH / 2, -400, SCUPPER_Z + SCUPPER_H / 2 - DOWNPIPE_R)
)
# === STEP 7: Aluminium coping cap on parapet top ===
cope_s = Part.makeBox(BUILDING_WIDTH, PARAPET_T + 50, COPING_T, Vector(0, -25, PARAPET_TOP_Z))
cope_n = Part.makeBox(BUILDING_WIDTH, PARAPET_T + 50, COPING_T, Vector(0, BUILDING_DEPTH - PARAPET_T - 25, PARAPET_TOP_Z))
cope_w = Part.makeBox(PARAPET_T + 50, BUILDING_DEPTH, COPING_T, Vector(-25, 0, PARAPET_TOP_Z))
cope_e = Part.makeBox(PARAPET_T + 50, BUILDING_DEPTH, COPING_T, Vector(BUILDING_WIDTH - PARAPET_T - 25, 0, PARAPET_TOP_Z))
# === ADD TO DOCUMENT ===
for shape, name in [
(roof_deck, "RoofDeck"),
(tapered_insul,"TaperedInsulation"),
(membrane, "Membrane"),
(par_s, "ParapetSouth"),
(par_n, "ParapetNorth"),
(par_w, "ParapetWest"),
(par_e, "ParapetEast"),
(downpipe, "Downpipe"),
(cope_s, "CopingSouth"),
(cope_n, "CopingNorth"),
(cope_w, "CopingWest"),
(cope_e, "CopingEast"),
]:
obj = doc.addObject("Part::Feature", name)
obj.Shape = shape
doc.recompute()
if FreeCAD.GuiUp:
FreeCAD.Gui.ActiveDocument.ActiveView.fitAll()
```
---
### Roof Eave Blocking
At the eave, the panel foam end is exposed. Solid timber blocking closes the gap and provides a nailing surface for fascia.
```python
EAVE_BLOCK_H = TOTAL_THICKNESS
eave_block = Part.makeBox(BUILDING_DEPTH, EAVE_BLOCK_H, 90,
Vector(0, 0, EAVE_HEIGHT - 90))
```
---
## 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).
```python
WALL_HEIGHT = BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H # total eave wall height
EAVE_HEIGHT = WALL_HEIGHT
# South wall (eave wall, Y=0 face): full BUILDING_WIDTH, rectangular to EAVE_HEIGHT
south_wall_origin = Vector(0, 0, 0) # runs in +X, thickness in +Y
south_wall_length = BUILDING_WIDTH
# North wall (eave wall, Y=BUILDING_DEPTH face): full BUILDING_WIDTH, rectangular
north_wall_origin = Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, 0)
north_wall_length = BUILDING_WIDTH
# West wall (GABLE END wall, X=0 face): BUILDING_DEPTH long, runs in +Y
# Rectangular base to EAVE_HEIGHT PLUS triangular gable section above — see Gable End Wall section
west_wall_origin = Vector(0, 0, 0) # exterior face at X=0, thickness in +X
west_wall_length = BUILDING_DEPTH # full depth — gable end spans full depth
# East wall (GABLE END wall, X=BUILDING_WIDTH face): BUILDING_DEPTH long, runs in +Y
# Same as west but mirrored — rectangular base + triangular gable section
east_wall_origin = Vector(BUILDING_WIDTH - PANEL_THICKNESS, 0, 0)
east_wall_length = BUILDING_DEPTH
```
**Walls running along Y-axis (east/west gable ends):** build a wall along X, then rotate 90° in Z, OR build directly by swapping the X and Y length parameters in the panel loop and orienting panels in the Y direction.
### Roof Slope Positions (Gable Roof, Ridge Along X)
```python
EAVE_HEIGHT = WALL_HEIGHT # eave walls top plate sits at this Z
# South slope: eave bottom face at Y = PANEL_THICKNESS (inner face of south eave wall)
# North slope: eave bottom face at Y = BUILDING_DEPTH - PANEL_THICKNESS (inner face of north eave wall)
# Ridge centre: Y = BUILDING_DEPTH / 2, Z = EAVE_HEIGHT + ridge_height
# See Duo-Pitch section above for correct placement formula
```
### 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
### 1. Concrete Slab (most common for SIP)
**The slab MUST extend beyond the wall footprint on all sides.** Use `SLAB_OVERHANG = 200mm` minimum. The wall bottom plates sit on the slab surface; the slab overhang provides bearing for the edge beam and prevents the wall foam from being at the slab edge.
```
Section view:
┌──────────────────────────────────────────────┐
│ SIP wall panel (foam min 150mm above ground)│
├──────────────────────────────────────────────┤ ← PT bottom plate (45×90mm)
├──────────────────────────────────────────────┤ ← DPC membrane (3mm)
├──────────────────────────────────────────────┤ ← slab top (Z=0)
│ concrete slab 125mm │
├─────┬────────────────────────────────┬───────┤
│edge │ insulation (optional) │ edge │
│beam │ │ beam │
│300× │ │ 300× │
│600 │ │ 600 │
└─────┴────────────────────────────────┴───────┘
← SLAB_OVERHANG (200mm min) →
← slab extends past wall face on all sides
```
```python
# === SLAB PARAMETERS ===
BUILDING_WIDTH = 3000 # outer wall face to outer wall face
BUILDING_DEPTH = 2000
SLAB_OVERHANG = 200 # slab extends beyond wall footprint — minimum 200mm
SLAB_W = BUILDING_WIDTH + 2 * SLAB_OVERHANG
SLAB_D = BUILDING_DEPTH + 2 * SLAB_OVERHANG
SLAB_T = 125 # slab field thickness
EDGE_BEAM_W = 300 # thickened perimeter beam width
EDGE_BEAM_D = 600 # total depth including slab thickness
DPC_T = 3 # damp proof course
CORE_THICKNESS = 150 # foam core of the SIP panel (SIP-150)
FACE_THICKNESS = 11 # OSB skin each side
SILL_PLATE_W = CORE_THICKNESS # plate slots into foam groove — NOT full SIP thickness
SILL_PLATE_H = 90
# Slab origin is SW corner of slab at slab bottom surface
# Z=0 = slab top (all wall and floor references start here)
slab_x0 = -SLAB_OVERHANG
slab_y0 = -SLAB_OVERHANG
# Slab field
slab = Part.makeBox(SLAB_W, SLAB_D, SLAB_T,
Vector(slab_x0, slab_y0, -SLAB_T))
# Edge beams (perimeter thickening)
eb_n = Part.makeBox(SLAB_W, EDGE_BEAM_W, EDGE_BEAM_D,
Vector(slab_x0, slab_y0 + SLAB_D - EDGE_BEAM_W, -EDGE_BEAM_D))
eb_s = Part.makeBox(SLAB_W, EDGE_BEAM_W, EDGE_BEAM_D,
Vector(slab_x0, slab_y0, -EDGE_BEAM_D))
eb_e = Part.makeBox(EDGE_BEAM_W, SLAB_D, EDGE_BEAM_D,
Vector(slab_x0 + SLAB_W - EDGE_BEAM_W, slab_y0, -EDGE_BEAM_D))
eb_w = Part.makeBox(EDGE_BEAM_W, SLAB_D, EDGE_BEAM_D,
Vector(slab_x0, slab_y0, -EDGE_BEAM_D))
foundation = slab.fuse(eb_n).fuse(eb_s).fuse(eb_e).fuse(eb_w)
# Anchor bolts M12 at 600mm centres (perimeter, modelled as cylinders)
BOLT_D = 12
BOLT_L = 150
for x_pos in range(300, BUILDING_WIDTH - 300, 600):
# South edge — bolt centred in foam channel (Y = FACE_THICKNESS + SILL_PLATE_W / 2)
bolt = Part.makeCylinder(BOLT_D / 2, BOLT_L,
Vector(x_pos, FACE_THICKNESS + SILL_PLATE_W / 2, -BOLT_L))
foundation = foundation.fuse(bolt)
# DPC membrane strip and sill plate sit at Z=0 on slab surface.
# Offset by FACE_THICKNESS so they align with the foam channel (OSB skins overhang on both sides).
dpc_s = Part.makeBox(BUILDING_WIDTH, SILL_PLATE_W, DPC_T,
Vector(0, FACE_THICKNESS, 0))
sp_s = Part.makeBox(BUILDING_WIDTH, SILL_PLATE_W, SILL_PLATE_H,
Vector(0, FACE_THICKNESS, DPC_T))
# Repeat for N, E, W walls
```
**Key rules for slab foundations:**
- Slab top surface = Z=0 (all wall/floor heights reference from this)
- `SLAB_OVERHANG` ≥ 200mm — if SLAB_W or SLAB_D equals BUILDING_WIDTH/DEPTH, the slab is too small
- Foam core bottom = DPC_T + SILL_PLATE_H = must be ≥ 150mm above finished ground (≈ top of edge beam)
- Anchor bolts: M12, 150mm embedment, 600mm centres, 150mm from corners
---
### 2. Strip Foundation
A single perimeter concrete strip foundation — a flat rectangular ring/frame sitting directly under all sole plates. Modelled as **one FreeCAD component** with agent id `foundation`. No T-shaped cross-section, no separate foundation wall segment, no individual side agents.
**NON-NEGOTIABLE: Strip foundations are ONE component, ONE agent (`foundation`), ONE FreeCAD model. Do NOT create four separate agents (`foundation_strip_south` etc.). Do NOT add a foundation wall box on top of the strip. The strip top face IS the sole plate bearing face at Z=0.**
**Z reference:** Z=0 is the top face of the strip (sole plate bearing face). Strip extends downward to Z=−STRIP_D.
**Plan geometry:** The strip is a hollow rectangular frame. Build it by fusing four flat boxes:
- South segment: full outer width × STRIP_W, placed at south edge
- North segment: full outer width × STRIP_W, placed at north edge
- West segment: STRIP_W wide × inner depth (between south and north segments)
- East segment: STRIP_W wide × inner depth (between south and north segments)
**Formula:**
- Outer width = `BUILDING_WIDTH + 2 × STRIP_W`
- Outer depth = `BUILDING_DEPTH + 2 × STRIP_W`
- Inner void starts at Y=STRIP_W (south), ends at Y=STRIP_W+BUILDING_DEPTH (north)
- All four segments are STRIP_D tall; top at Z=0, bottom at Z=−STRIP_D
```python
# === STRIP FOUNDATION PARAMETERS ===
BUILDING_WIDTH = 4000 # outer wall face to outer wall face (from spec)
BUILDING_DEPTH = 3000
STRIP_W = 450 # strip width (plan dimension, perpendicular to wall)
STRIP_D = 300 # strip depth (total buried concrete thickness)
# Z=0 = top of strip = sole plate bearing face
# Strip runs from Z=0 down to Z=-STRIP_D (flat, no foundation wall above it)
# South segment — full outer width, south edge
seg_s = Part.makeBox(BUILDING_WIDTH + 2 * STRIP_W, STRIP_W, STRIP_D,
Vector(-STRIP_W, -STRIP_W, -STRIP_D))
# North segment — full outer width, north edge
seg_n = Part.makeBox(BUILDING_WIDTH + 2 * STRIP_W, STRIP_W, STRIP_D,
Vector(-STRIP_W, BUILDING_DEPTH, -STRIP_D))
# West segment — between south and north, west edge
seg_w = Part.makeBox(STRIP_W, BUILDING_DEPTH, STRIP_D,
Vector(-STRIP_W, 0, -STRIP_D))
# East segment — between south and north, east edge
seg_e = Part.makeBox(STRIP_W, BUILDING_DEPTH, STRIP_D,
Vector(BUILDING_WIDTH, 0, -STRIP_D))
# Fuse into single perimeter ring
foundation = seg_s.fuse(seg_n).fuse(seg_w).fuse(seg_e)
feature = doc.addObject("Part::Feature", "StripFoundation")
feature.Shape = foundation
```
**Key rules for strip foundations:**
- ONE agent (`foundation`), ONE model — never split into four side agents
- No foundation wall segment — the strip top face at Z=0 is the bearing face for sole plates
- South and north segments span `BUILDING_WIDTH + 2 × STRIP_W`; east and west span `BUILDING_DEPTH`
- All four segments are the same depth (STRIP_D); top face exactly at Z=0
- Cladding overhangs the foundation face by 40–50mm — the strip exterior face is flush with the SIP wall exterior face
---
### 3. Screw Pile Foundation
Steel screw piles driven to bearing depth, carrying LVL bearer beams. Best for sloped sites and remote/off-grid builds.
```python
# === SCREW PILE PARAMETERS ===
PILE_DIAMETER = 114
PILE_SPACING_X = 2400
PILE_SPACING_Y = 2400
PILE_EXPOSED_H = 600
BEARER_W = 90
BEARER_H = 190
piles_x = (BUILDING_WIDTH // PILE_SPACING_X) + 1
piles_y = (BUILDING_DEPTH // PILE_SPACING_Y) + 1
parts = []
for ix in range(piles_x):
for iy in range(piles_y):
x = ix * PILE_SPACING_X
y = iy * PILE_SPACING_Y
pile = Part.makeCylinder(PILE_DIAMETER / 2, PILE_EXPOSED_H,
Vector(x, y, -PILE_EXPOSED_H))
parts.append(pile)
# Bearer beams spanning across building width
for iy in range(piles_y):
y = iy * PILE_SPACING_Y
b1 = Part.makeBox(BUILDING_WIDTH, BEARER_W, BEARER_H, Vector(0, y, 0))
b2 = Part.makeBox(BUILDING_WIDTH, BEARER_W, BEARER_H,
Vector(0, y + BEARER_W + 10, 0))
parts.extend([b1, b2])
```
---
## 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_HIGH + MEMBRANE_T # through taper + membrane
terminal_void = Part.makeCylinder(
TERMINAL_R, penetration_depth,
Vector(TERM_X, TERM_Y, insul_z_base)
)
tapered_insul = tapered_insul.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 + TAPER_INSUL_T + MEMBRANE_T
- `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, TAPER_INSUL_T=40, 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 flat roof SIP panels to create drainage fall** — build panels horizontal and achieve fall with tapered insulation on top; tilting panels makes wall height coordination impossible
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. **Tapered insulation is a wedge, never a flat box** — use a polygon face extrude to create the wedge; a flat box produces no fall and the roof will pond water
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; each wall component starts with `bp = Part.makeBox(WALL_LENGTH, TOTAL_THICKNESS, BOTTOM_PLATE_H, Vector(0, 0, 0))` at Z=0 before the panels
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 = 18` as the design value** — 18mm is the absolute code floor (BS 8217). Use `INSUL_MIN = 50` as the design minimum. A tapered insulation wedge that goes from 18mm to ~59mm (1:40 fall over 1.6m) is barely visible in the model and leaves no margin for deflection
29. **Never use 1:40 as the design fall** — 1:40 is the code minimum; use `FALL_HEIGHT = INNER_DEPTH / 20` (1:20 design fall) in all generated models. Over a 1.6m inner span, 1:20 gives ~83mm height difference; 1:40 gives only ~41mm — less than half as visible and with no tolerance margin
30. **The membrane must be a wedge, not a flat box** — model it as a parallel polygon extrude on top of the insulation surface, same profile in Y-Z. A flat `Part.makeBox` for the membrane makes the roof surface look completely flat regardless of the tapered insulation below it
31. **Tapered insulation must be a polygon extrude, never a box** — `Part.makeBox(…, INSUL_HIGH)` creates a block of uniform height; only a trapezoid profile extruded in X (or Y) creates a genuine wedge with the slope the drainage design requires. A box will make the roof look flat even though it has the wrong height
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.
# SIP Wall Construction Skill
## 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.
---
## Panel FreeCAD Code Pattern
Model each SIP panel as a 3-layer fused solid: two OSB faces + EPS foam core.
```python
import FreeCAD, Part, math
from FreeCAD import Vector
# === PARAMETERS ===
PANEL_WIDTH = 1200 # mm
PANEL_HEIGHT = 2700 # mm
CORE_THICKNESS = 150 # mm (SIP-150)
FACE_THICKNESS = 11 # mm OSB each side
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS # 172mm
# === BUILD PANEL ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("SIP_Panel")
face1 = Part.makeBox(PANEL_WIDTH, FACE_THICKNESS, PANEL_HEIGHT,
Vector(0, 0, 0))
core = Part.makeBox(PANEL_WIDTH, CORE_THICKNESS, PANEL_HEIGHT,
Vector(0, FACE_THICKNESS, 0))
face2 = Part.makeBox(PANEL_WIDTH, FACE_THICKNESS, PANEL_HEIGHT,
Vector(0, FACE_THICKNESS + CORE_THICKNESS, 0))
panel = face1.fuse(core).fuse(face2)
obj = doc.addObject("Part::Feature", "SIP_Panel")
obj.Shape = panel
doc.recompute()
```
---
## 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
**Every wall is a sequence of individual 1200mm SIP panels joined by block splines, sitting on a pressure-treated bottom plate. Never model a wall as a single box.**
```
┌─────────────────────────────────────┐ ← double top plate (2× 45×90mm) Z = BOTTOM_PLATE_H + PANEL_HEIGHT
│ Panel │Sp│ Panel │Sp│ Panel │ ← SIP panels Z = BOTTOM_PLATE_H (= 90)
└─────────────────────────────────────┘ ← PT bottom plate (45×90mm) Z = 0
══════════════════════════════════════ ← slab top / floor level Z = 0
```
**Bottom plate (mandatory — generated by the wall component, not the foundation):**
45 × 90mm pressure-treated timber, full wall length, sits at Z=0 on the slab DPC. The SIP panels start at Z=90 on top of this plate. If the wall code does not include a bottom plate at Z=0, the panels will be floating directly on concrete — this is wrong.
**Top plate:** Double 45 × 90mm timber, full wall length, nailed through OSB face into foam.
**Wall height** = BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H = 90 + panel_height + 180mm
### Panel Count Calculation
Always calculate how many full 1200mm panels fit and what remainder is needed:
```python
full_panels = WALL_LENGTH // 1200 # number of full-width panels
remainder = WALL_LENGTH % 1200 # width of last custom panel (0 = exact fit)
panel_count = full_panels + (1 if remainder > 0 else 0)
panel_widths = [1200] * full_panels + ([remainder] if remainder > 0 else [])
```
Common wall lengths:
| Wall length | Full panels | Remainder panel |
|-------------|-------------|-----------------|
| 2400mm | 2 | none |
| 3000mm | 2 | 600mm |
| 3600mm | 3 | none |
| 4800mm | 4 | none |
| 6000mm | 5 | none |
| 9000mm | 7 | 600mm |
### Wall Assembly Code
```python
import FreeCAD, Part
from FreeCAD import Vector
# === WALL PARAMETERS ===
WALL_LENGTH = 3000 # example: 3m wall = 2 full panels + 1×600mm remainder
PANEL_HEIGHT = 2700
CORE_THICKNESS = 150
FACE_THICKNESS = 11
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS # 172mm for SIP-150
BOTTOM_PLATE_H = 90 # PT timber bottom plate — MANDATORY
TOP_PLATE_H = 180 # double top plate (2× 45mm)
SPLINE_W = 45
SPLINE_D = 90
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Wall")
# --- Panel widths (calculated, not hardcoded) ---
full_panels = WALL_LENGTH // 1200
remainder = WALL_LENGTH % 1200
panel_widths = [1200] * full_panels + ([remainder] if remainder > 0 else [])
# === STEP 1: Bottom plate (MANDATORY — wall sits on this, not directly on slab) ===
# Depth = CORE_THICKNESS (not TOTAL_THICKNESS) — plate slots into the foam groove.
# OSB skins (FACE_THICKNESS each) overhang on both faces; offset plate by FACE_THICKNESS.
bp = Part.makeBox(WALL_LENGTH, CORE_THICKNESS, BOTTOM_PLATE_H, Vector(0, FACE_THICKNESS, 0))
parts = [bp]
# === STEP 2: SIP panels and block splines — one panel per entry in panel_widths ===
x = 0
for i, pw in enumerate(panel_widths):
# Three-layer SIP panel
face1 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT,
Vector(x, 0, BOTTOM_PLATE_H))
core = Part.makeBox(pw, CORE_THICKNESS, PANEL_HEIGHT,
Vector(x, FACE_THICKNESS, BOTTOM_PLATE_H))
face2 = Part.makeBox(pw, FACE_THICKNESS, PANEL_HEIGHT,
Vector(x, FACE_THICKNESS + CORE_THICKNESS, BOTTOM_PLATE_H))
parts.extend([face1, core, face2])
x += pw
# Block spline at joint between this panel and the next
if i < len(panel_widths) - 1:
sp = Part.makeBox(SPLINE_W, SPLINE_D, PANEL_HEIGHT,
Vector(x - SPLINE_W / 2,
(TOTAL_THICKNESS - SPLINE_D) / 2,
BOTTOM_PLATE_H))
parts.append(sp)
# === STEP 3: Double top plate ===
# Same rule as bottom plate: CORE_THICKNESS depth, 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)
wall = parts[0]
for p in parts[1:]:
wall = wall.fuse(p)
feature = doc.addObject("Part::Feature", "Wall")
feature.Shape = wall
doc.recompute()
```
---
## 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.
---
## MANDATORY: Panel Span Splitting — Walls
Standard SIP stock is 2440mm × 1220mm. At wall heights above 2440mm, the panel must be vertically split.
### 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))
```
---
## ⚠ Non-Negotiable Wall Rules
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.
7. **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))
```
SIP Wall Construction
Native
SIP panel specs, spline types, wall assembly code, corner conditions, and plate rules. Injected to agents building wall components.
~2728 tokens
# SIP Roof Construction Skill
This skill defines how to design and model SIP roof components in FreeCAD using the direct modeling approach. It covers roof panel specifications, ridge bevels, mono-pitch and duo-pitch roofs, gable sections, flat roof drainage, and span splitting — all in metric units.
---
## SIP Panel Specifications (Metric)
All dimensions in millimetres. OSB facing is 11mm each side.
| Name | Total Thickness | Core (EPS) | Approx. R-Value |
|--------------|-----------------|------------|-----------------|
| SIP-100 | 122mm | 100mm | R-15 |
| SIP-150 | 172mm | 150mm | R-23 |
| SIP-200 | 222mm | 200mm | R-30 |
| SIP-250 | 272mm | 250mm | R-38 |
| SIP-300 | 322mm | 300mm | R-45 |
**Standard panel widths:** 1200mm (preferred) or 1220mm
**Standard panel heights:** 2400mm, 2700mm, 3000mm (custom heights available)
**OSB face thickness:** 11mm each side
Wall panels orient with height vertical (Z-axis). Roof panels orient with height along slope.
---
## Roof Panels
SIP roof panels are the same composite construction as wall panels but oriented at the roof pitch. The slope is defined by the pitch angle.
### Ridge Bevel — CRITICAL for Watertight Fit
Without a bevel cut at the ridge, the two slope panels overrun the centreline and cannot mate flush. Always cut the bevel in the flat panel before applying the placement rotation.
**Panel local frame (all pitched roof code uses this):**
- X = 0 to BUILDING_WIDTH (along ridge)
- Y = 0 to slope_length (along slope, 0 = eave end, slope_length = ridge end)
- Z = 0 to TOTAL_THICKNESS (0 = interior/bottom face, TT = exterior/top face)
**Rule:** cut a triangular prism from the ridge end (Y = slope_length) in the YZ plane, extruded along X. After rotating the flat panel to pitch angle, this cut face becomes plumb (vertical), and the two slope faces mate flush at the apex.
```
Flat panel cross-section in YZ plane (before rotation):
←── slope_length ──────────────── bevel ──┐
──────────────────────────────────────────│ Z = 0 (interior face)
│
──────────────────────────────╱───────────│ Z = TT (exterior face)
↑
ridge end: exterior face is cut back by bevel_y = TT × tan(pitch)
interior face stays at Y = slope_length
```
**Formulae:**
- `bevel_y = TOTAL_THICKNESS * tan(PITCH_DEG)` — ridge bevel setback at exterior face
- `eave_bevel_y = TOTAL_THICKNESS / tan(PITCH_DEG)` — eave bevel setback at interior face
```python
import math, Part
from FreeCAD import Vector, Placement, Rotation
PITCH_RAD = math.radians(PITCH_DEG)
# Panel builder: X = ridge_length, Y = slope_length, Z = thickness
def make_slope_panel(ridge_len, slope_len, face_t, core_t):
f1 = Part.makeBox(ridge_len, slope_len, face_t)
c = Part.makeBox(ridge_len, slope_len, core_t, Vector(0, 0, face_t))
f2 = Part.makeBox(ridge_len, slope_len, face_t, Vector(0, 0, face_t + core_t))
return f1.fuse(c).fuse(f2)
# Ridge bevel: cut triangle at Y = slope_length end (YZ plane, extruded in X)
# Triangle: (sl - bevel_y, TT), (sl, TT), (sl, 0) — removes exterior overhang
def cut_ridge_bevel_slope(panel, sl, bevel_y, ridge_len, tt):
pts = [Vector(0, sl - bevel_y, tt),
Vector(0, sl, tt),
Vector(0, sl, 0),
Vector(0, sl - bevel_y, tt)]
return panel.cut(Part.Face(Part.makePolygon(pts)).extrude(Vector(ridge_len, 0, 0)))
# Eave bevel: cut triangle at Y = 0 end so panel bears on angled-cut top plate
# Triangle: (0, 0), (eave_bv, 0), (0, TT) — removes interior corner at eave
def cut_eave_bevel_slope(panel, eave_bv, ridge_len, tt):
pts = [Vector(0, 0, 0),
Vector(0, eave_bv, 0),
Vector(0, 0, tt),
Vector(0, 0, 0)]
return panel.cut(Part.Face(Part.makePolygon(pts)).extrude(Vector(ridge_len, 0, 0)))
```
### Mono-Pitch Roof
Single slope from low eave wall to high wall (or ridge). Uses the same panel local frame as duo-pitch: X = ridge length (BUILDING_WIDTH), Y = along slope, Z = thickness.
```python
# === MONO-PITCH PARAMETERS ===
BUILDING_WIDTH = 6000 # ridge/eave length (X direction)
BUILDING_DEPTH = 4000 # horizontal span from eave to high wall (Y direction)
PITCH_DEG = 20
PANEL_WIDTH_ROOF = 1200
CORE_THICKNESS = 150
FACE_THICKNESS = 11
TOTAL_THICKNESS = CORE_THICKNESS + 2 * FACE_THICKNESS
EAVE_HEIGHT = 2960 # Z of top of low eave wall top plate
PITCH_RAD = math.radians(PITCH_DEG)
slope_length = BUILDING_DEPTH / math.cos(PITCH_RAD) # along-slope distance
rise = BUILDING_DEPTH * math.tan(PITCH_RAD) # height gained across span
bevel_y = TOTAL_THICKNESS * math.tan(PITCH_RAD) # ridge bevel setback
eave_bevel_y = TOTAL_THICKNESS / math.tan(PITCH_RAD) # eave bevel setback
n_panels = math.ceil(slope_length / PANEL_WIDTH_ROOF)
roof_parts = []
y_pos = 0.0
for i in range(n_panels):
pw = min(PANEL_WIDTH_ROOF, slope_length - y_pos)
p = make_slope_panel(BUILDING_WIDTH, pw, FACE_THICKNESS, CORE_THICKNESS)
p.translate(Vector(0, y_pos, 0))
if i == n_panels - 1: # ridge/high-wall end bevel
p = cut_ridge_bevel_slope(p, pw, bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
if i == 0: # eave end bevel
p = cut_eave_bevel_slope(p, eave_bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
roof_parts.append(p)
y_pos += pw
roof = roof_parts[0]
for p in roof_parts[1:]:
roof = roof.fuse(p)
# Rotation by +PITCH_DEG around X tilts the Y (slope) axis upward into +Z.
# Base places the eave bottom corner (local 0,0,0) at the inner face of the low wall top plate.
rot = Rotation(Vector(1, 0, 0), PITCH_DEG)
pl = Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot)
roof_obj = doc.addObject("Part::Feature", "Roof")
roof_obj.Shape = roof
roof_obj.Placement = pl
```
### Duo-Pitch (Gable) Roof
Two slopes meeting at a central ridge beam. Each slope is built in the SAME local frame and gets a ridge bevel so the faces mate flush at the apex. South and north placements differ — do not copy one from the other with a simple sign flip.
**Orientation convention:** ridge runs along BUILDING_WIDTH (X), slope spans BUILDING_DEPTH (Y). South slope eave at Y ≈ PANEL_THICKNESS; north slope eave at Y ≈ BUILDING_DEPTH − PANEL_THICKNESS; ridge at Y = BUILDING_DEPTH/2.
**Panel local frame:** X = 0…BUILDING_WIDTH (ridge direction), Y = 0…slope_length (slope, 0 = eave, slope_length = ridge end), Z = 0…TOTAL_THICKNESS (0 = interior/bottom face, TT = exterior/top face).
```python
# === DUO-PITCH PARAMETERS ===
HALF_SPAN = BUILDING_DEPTH / 2 # horizontal half-span (eave to ridge centreline)
slope_length = HALF_SPAN / math.cos(PITCH_RAD)
ridge_height = HALF_SPAN * math.tan(PITCH_RAD)
RIDGE_Z = EAVE_HEIGHT + ridge_height
# Ridge bevel: cut wedge from ridge end (Y = slope_length) so the face becomes
# vertical after the slope is rotated to pitch. bevel_y = TT * tan(P)
bevel_y = TOTAL_THICKNESS * math.tan(PITCH_RAD)
# Eave bevel: cut wedge from eave end (Y = 0) so the panel bears cleanly on the
# angled-cut top plate. eave_bevel_y = TT / tan(P) = TT * cot(P)
eave_bevel_y = TOTAL_THICKNESS / math.tan(PITCH_RAD)
# --- Panel builder (X = ridge, Y = slope, Z = thickness) ---
def make_slope_panel(bw, sl, face_t, core_t):
f1 = Part.makeBox(bw, sl, face_t)
c = Part.makeBox(bw, sl, core_t, Vector(0, 0, face_t))
f2 = Part.makeBox(bw, sl, face_t, Vector(0, 0, face_t + core_t))
return f1.fuse(c).fuse(f2)
# Ridge bevel cut — removes triangle at Y = slope_length end (in YZ plane, extruded along X)
def cut_ridge_bevel_slope(panel, sl, bv_y, bw, tt):
pts = [Vector(0, sl - bv_y, tt),
Vector(0, sl, tt),
Vector(0, sl, 0),
Vector(0, sl - bv_y, tt)]
face = Part.Face(Part.makePolygon(pts))
return panel.cut(face.extrude(Vector(bw, 0, 0)))
# Eave bevel cut — removes triangle at Y = 0 end
def cut_eave_bevel_slope(panel, eb_y, bw, tt):
pts = [Vector(0, 0, 0),
Vector(0, eb_y, 0),
Vector(0, 0, tt),
Vector(0, 0, 0)]
face = Part.Face(Part.makePolygon(pts))
return panel.cut(face.extrude(Vector(bw, 0, 0)))
# --- Build south slope ---
n_panels = math.ceil(slope_length / PANEL_WIDTH_ROOF)
south_parts = []
y_pos = 0.0
for i in range(n_panels):
pw = min(PANEL_WIDTH_ROOF, slope_length - y_pos)
p = make_slope_panel(BUILDING_WIDTH, pw, FACE_THICKNESS, CORE_THICKNESS)
p.translate(Vector(0, y_pos, 0))
if i == n_panels - 1:
p = cut_ridge_bevel_slope(p, pw, bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
if i == 0:
p = cut_eave_bevel_slope(p, eave_bevel_y, BUILDING_WIDTH, TOTAL_THICKNESS)
south_parts.append(p)
y_pos += pw
south_roof = south_parts[0]
for p in south_parts[1:]:
south_roof = south_roof.fuse(p)
# South placement: +PITCH_DEG around X tilts the Y (slope) axis up toward +Z.
# Base puts the eave bottom corner (local 0,0,0) at the inner face of south wall top.
rot_s = Rotation(Vector(1, 0, 0), PITCH_DEG)
pl_s = Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot_s)
south_obj = doc.addObject("Part::Feature", "RoofSouthSlope")
south_obj.Shape = south_roof
south_obj.Placement = pl_s
# --- North slope (same panel geometry, different placement) ---
north_roof = south_roof.copy()
# North placement: (180 - PITCH_DEG) around X maps local +Y to world -Y direction,
# so Y=0 (eave end) stays at the north wall inner face and Y=slope_length (ridge end)
# reaches world Y = BUILDING_DEPTH/2. Do NOT use -PITCH_DEG — that sends the ridge
# further away from centre, not toward it.
rot_n = Rotation(Vector(1, 0, 0), 180 - PITCH_DEG)
pl_n = Placement(Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, EAVE_HEIGHT), rot_n)
north_obj = doc.addObject("Part::Feature", "RoofNorthSlope")
north_obj.Shape = north_roof
north_obj.Placement = pl_n
# --- Ridge beam (LVL or glulam, vertical, centred at ridge) ---
RIDGE_BEAM_D = max(200, int(HALF_SPAN / 10)) # depth (Z), min 200mm
RIDGE_BEAM_W = 90 # width (Y)
ridge = Part.makeBox(
BUILDING_WIDTH,
RIDGE_BEAM_W,
RIDGE_BEAM_D,
Vector(0, BUILDING_DEPTH / 2 - RIDGE_BEAM_W / 2, RIDGE_Z)
)
ridge_obj = doc.addObject("Part::Feature", "RidgeBeam")
ridge_obj.Shape = ridge
```
**Key placement sanity check (substitute real numbers to verify):**
- South eave bottom at world (x, PANEL_THICKNESS, EAVE_HEIGHT) ✓
- South ridge bottom at world (x, PANEL_THICKNESS + HALF_SPAN, RIDGE_Z) ≈ (x, BUILDING_DEPTH/2, RIDGE_Z) ✓
- North eave bottom at world (x, BUILDING_DEPTH − PANEL_THICKNESS, EAVE_HEIGHT) ✓
- North ridge bottom at world (x, BUILDING_DEPTH/2 + PANEL_THICKNESS − small, RIDGE_Z) ≈ (x, BUILDING_DEPTH/2, RIDGE_Z) ✓
### Gable End Wall — Triangular Section
For a duo-pitch roof (ridge along X), the **east and west walls** are gable end walls. They must include the full triangular gable section above EAVE_HEIGHT. Build each gable wall component as two fused solids:
1. **Rectangular base** — standard SIP panel loop from Z=0 to Z=EAVE_HEIGHT (full BUILDING_DEPTH length)
2. **Triangular gable prism** — above EAVE_HEIGHT, triangular cross-section in the YZ plane, extruded PANEL_THICKNESS in X
```python
# Triangular gable section for east or west gable end wall
# Cross-section (YZ plane) triangle:
# (Y=0, Z=EAVE_HEIGHT) ← south corner (eave height)
# (Y=BUILDING_DEPTH/2, Z=RIDGE_Z) ← apex (ridge height)
# (Y=BUILDING_DEPTH, Z=EAVE_HEIGHT) ← north corner (eave height)
gable_pts = [
Vector(0, 0, EAVE_HEIGHT),
Vector(0, BUILDING_DEPTH / 2, RIDGE_Z),
Vector(0, BUILDING_DEPTH, EAVE_HEIGHT),
Vector(0, 0, EAVE_HEIGHT), # close
]
gable_wire = Part.makePolygon(gable_pts)
gable_face = Part.Face(gable_wire)
gable_prism = gable_face.extrude(Vector(PANEL_THICKNESS, 0, 0))
# West gable wall (exterior face at X = 0):
# The rectangular base wall is already built in the wall loop at X=0.
# Fuse the gable prism on top — it sits at X=0 to X=PANEL_THICKNESS, Y=0 to BUILDING_DEPTH.
west_gable = wall_solid.fuse(gable_prism) # wall_solid = rectangular section
# East gable wall (exterior face at X = BUILDING_WIDTH):
gable_prism_east = gable_prism.copy()
gable_prism_east.translate(Vector(BUILDING_WIDTH - PANEL_THICKNESS, 0, 0))
east_gable = wall_solid_east.fuse(gable_prism_east)
```
**The angled top face of the gable prism naturally matches the roof pitch** — no additional cut needed. The face slope from (0, EAVE_HEIGHT) to (BUILDING_DEPTH/2, RIDGE_Z) is `rise / run = ridge_height / HALF_SPAN = tan(PITCH_DEG)` ✓
### Flat Roof
A flat roof uses horizontal SIP panels resting directly on the wall top plates. Use SIP-200 or SIP-250 for flat roofs — the additional insulation compensates for the reduced stack effect and improves thermal performance at the cold deck.
**Drainage fall is not optional — a truly flat surface ponds water and will fail.** The slope is created by tapered insulation on top of the horizontal SIP deck; the SIP deck itself stays level. Do not tilt the structural panels — it makes wall height coordination impossible.
**Fall rates:**
- **1:40 (25mm/m)** — absolute code minimum (BS 8217). Produces ~40mm height difference over a 1.6m inner span; barely visible in a 3D model and leaves no margin for deflection or construction tolerance.
- **1:20 (50mm/m)** — **design target.** Use this for all models. Produces ~80mm height difference over a 1.6m inner span, clearly visible, and ensures drainage even after some deflection occurs in service.
**Always use 1:20 in generated models.** The formula is `FALL_HEIGHT = INNER_DEPTH / 20`.
#### Flat Roof Elements
1. **SIP roof panels** — horizontal, spanning the shorter building dimension. Multiple 1200mm-wide panels joined by splines, same panel-loop pattern as walls.
2. **Tapered insulation board** — modelled as a **wedge** (thick at high side, thin at low side) using a polygon-face extrude. Use `FALL_RATIO = 20` (1:20 design fall). Minimum thickness at the low point: **50mm** (absolute code floor is 18mm but 50mm gives visible slope and prevents ponding in construction tolerances).
3. **Waterproof membrane** — EPDM or TPO, 3mm shell on top of the insulation.
4. **Parapet walls** — short SIP-100 panels extending above roof level on all four sides. The interior face of the parapet is lined with the membrane upstand.
5. **Scupper outlets through the parapet** — rectangular holes cut through the parapet panel at the low side of the fall. These are the only way water can leave a parapeted roof. Without scuppers the roof will flood.
6. **Overflow scuppers** — a second set of scupper holes 50mm higher than the primary, as a backup if the primary blocks.
7. **Downpipe stubs** — short cylinder representing the external downpipe at each primary scupper.
8. **Flat roof bearer (optional)** — for spans > 3000mm, a mid-span LVL bearer under the panels reduces deflection.
#### Parapet vs. Open Eave
| Detail | Parapet | Open Eave / Fascia |
|--------------|----------------------------------|--------------------------------|
| Look | Modern, clean — walls continue past roof | Reveals roof edge from below |
| Water | Contained; internal drainage | Drains freely at edge |
| Model | Extend wall panels up by PARAPET_H above roof | Fascia board at panel end |
| Default | **Use this for modern flat roofs** | Use for agricultural/industrial |
#### Flat Roof Code
```python
import FreeCAD, Part, math
from FreeCAD import Vector
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("FlatRoof")
# === PARAMETERS ===
BUILDING_WIDTH = 3000 # outer face to outer face (X)
BUILDING_DEPTH = 2000 # outer face to outer face (Y)
WALL_HEIGHT = 2970 # BOTTOM_PLATE_H + PANEL_HEIGHT + TOP_PLATE_H
PANEL_THICKNESS = 172 # wall SIP total thickness
ROOF_CORE = 200 # SIP-200 for flat roofs
ROOF_FACE = 11
ROOF_THICKNESS = ROOF_CORE + 2 * ROOF_FACE # 222mm
ROOF_PANEL_W = 1200
INSUL_MIN = 50 # insulation at low point (mm) — 50mm design minimum; code floor is 18mm
FALL_RATIO = 20 # 1:20 design fall — use this, not 1:40 (code minimum only)
MEMBRANE_T = 3 # EPDM/TPO membrane
PARAPET_H = 300 # parapet height above ROOF_Z
PARAPET_T = 122 # SIP-100 parapet panels
COPING_T = 3 # aluminium coping
# === DRAINAGE GEOMETRY ===
# Fall direction: high side at Y = BUILDING_DEPTH - PANEL_THICKNESS (back/north)
# low side at Y = PANEL_THICKNESS (front/south) — scuppers here
INNER_WIDTH = BUILDING_WIDTH - 2 * PANEL_THICKNESS
INNER_DEPTH = BUILDING_DEPTH - 2 * PANEL_THICKNESS
FALL_HEIGHT = INNER_DEPTH / FALL_RATIO # 1:20 design fall — e.g. 80mm over 1600mm inner depth
INSUL_HIGH = INSUL_MIN + FALL_HEIGHT # thickness at high side
# Confirm the slope is visible: FALL_HEIGHT should be >> 0
# e.g. 3000×2000 building: INNER_DEPTH≈1656mm → FALL_HEIGHT≈83mm, INSUL range 50→133mm
ROOF_Z = WALL_HEIGHT # bottom face of SIP roof panels
insul_z_base = ROOF_Z + ROOF_THICKNESS # top of roof deck = base of insulation
# === STEP 1: SIP Roof Deck (multiple panels — same loop as walls) ===
full_panels = INNER_DEPTH // ROOF_PANEL_W
remainder = INNER_DEPTH % ROOF_PANEL_W
panel_widths_roof = [ROOF_PANEL_W] * full_panels + ([remainder] if remainder > 0 else [])
roof_parts = []
y = PANEL_THICKNESS
for i, pw in enumerate(panel_widths_roof):
f1 = Part.makeBox(INNER_WIDTH, pw, ROOF_FACE,
Vector(PANEL_THICKNESS, y, ROOF_Z))
co = Part.makeBox(INNER_WIDTH, pw, ROOF_CORE,
Vector(PANEL_THICKNESS, y, ROOF_Z + ROOF_FACE))
f2 = Part.makeBox(INNER_WIDTH, pw, ROOF_FACE,
Vector(PANEL_THICKNESS, y, ROOF_Z + ROOF_FACE + ROOF_CORE))
roof_parts.append(f1.fuse(co).fuse(f2))
if i < len(panel_widths_roof) - 1:
sp = Part.makeBox(INNER_WIDTH, 45, 90,
Vector(PANEL_THICKNESS, y + pw - 22.5,
ROOF_Z + (ROOF_THICKNESS - 90) / 2))
roof_parts.append(sp)
y += pw
roof_deck = roof_parts[0]
for p in roof_parts[1:]:
roof_deck = roof_deck.fuse(p)
# === STEP 2: Tapered insulation — WEDGE, not a flat box ===
# Profile in Y-Z plane, extruded in X.
# Low side (front, Y = PANEL_THICKNESS): insul_z_base + INSUL_MIN
# High side (back, Y = BUILDING_DEPTH - PANEL_THICKNESS): insul_z_base + INSUL_HIGH
insul_pts = [
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base + INSUL_HIGH),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base),
]
insul_wire = Part.makePolygon(insul_pts)
insul_face = Part.Face(insul_wire)
tapered_insul = insul_face.extrude(Vector(INNER_WIDTH, 0, 0))
# === STEP 3: Membrane — wedge following insulation slope, same profile in Y-Z plane ===
# Must be a wedge, not a flat box, so the finished roof surface reads as sloped in the model.
mem_pts = [
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base + INSUL_HIGH),
Vector(PANEL_THICKNESS, BUILDING_DEPTH - PANEL_THICKNESS, insul_z_base + INSUL_HIGH + MEMBRANE_T),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN + MEMBRANE_T),
Vector(PANEL_THICKNESS, PANEL_THICKNESS, insul_z_base + INSUL_MIN),
]
mem_wire = Part.makePolygon(mem_pts)
mem_face = Part.Face(mem_wire)
membrane = mem_face.extrude(Vector(INNER_WIDTH, 0, 0))
# === STEP 4: Parapet walls (four sides) ===
PARAPET_Z = ROOF_Z # parapets start at same Z as roof panels (wall top)
PARAPET_TOP_Z = PARAPET_Z + PARAPET_H
par_s = Part.makeBox(BUILDING_WIDTH, PARAPET_T, PARAPET_H,
Vector(0, 0, PARAPET_Z))
par_n = Part.makeBox(BUILDING_WIDTH, PARAPET_T, PARAPET_H,
Vector(0, BUILDING_DEPTH - PARAPET_T, PARAPET_Z))
par_w = Part.makeBox(PARAPET_T, BUILDING_DEPTH - 2 * PARAPET_T, PARAPET_H,
Vector(0, PARAPET_T, PARAPET_Z))
par_e = Part.makeBox(PARAPET_T, BUILDING_DEPTH - 2 * PARAPET_T, PARAPET_H,
Vector(BUILDING_WIDTH - PARAPET_T, PARAPET_T, PARAPET_Z))
# === STEP 5: Scuppers through south parapet at low side — MANDATORY ===
# Primary scupper: centred on south parapet, at membrane level
SCUPPER_W = 100 # opening width
SCUPPER_H = 75 # opening height
SCUPPER_Z = insul_z_base + INSUL_MIN - 10 # just below low-point membrane surface
scupper_primary = Part.makeBox(
SCUPPER_W, PARAPET_T, SCUPPER_H,
Vector(BUILDING_WIDTH / 2 - SCUPPER_W / 2, 0, SCUPPER_Z)
)
par_s = par_s.cut(scupper_primary)
# Overflow scupper: offset to one side, 50mm higher (backup if primary blocks)
scupper_overflow = Part.makeBox(
SCUPPER_W, PARAPET_T, SCUPPER_H,
Vector(BUILDING_WIDTH / 4 - SCUPPER_W / 2, 0, SCUPPER_Z + 50)
)
par_s = par_s.cut(scupper_overflow)
# === STEP 6: Downpipe stub at primary scupper (exterior face) ===
DOWNPIPE_R = 50 # 100mm dia downpipe
downpipe = Part.makeCylinder(
DOWNPIPE_R, 400,
Vector(BUILDING_WIDTH / 2, -400, SCUPPER_Z + SCUPPER_H / 2 - DOWNPIPE_R)
)
# === STEP 7: Aluminium coping cap on parapet top ===
cope_s = Part.makeBox(BUILDING_WIDTH, PARAPET_T + 50, COPING_T, Vector(0, -25, PARAPET_TOP_Z))
cope_n = Part.makeBox(BUILDING_WIDTH, PARAPET_T + 50, COPING_T, Vector(0, BUILDING_DEPTH - PARAPET_T - 25, PARAPET_TOP_Z))
cope_w = Part.makeBox(PARAPET_T + 50, BUILDING_DEPTH, COPING_T, Vector(-25, 0, PARAPET_TOP_Z))
cope_e = Part.makeBox(PARAPET_T + 50, BUILDING_DEPTH, COPING_T, Vector(BUILDING_WIDTH - PARAPET_T - 25, 0, PARAPET_TOP_Z))
# === ADD TO DOCUMENT ===
for shape, name in [
(roof_deck, "RoofDeck"),
(tapered_insul,"TaperedInsulation"),
(membrane, "Membrane"),
(par_s, "ParapetSouth"),
(par_n, "ParapetNorth"),
(par_w, "ParapetWest"),
(par_e, "ParapetEast"),
(downpipe, "Downpipe"),
(cope_s, "CopingSouth"),
(cope_n, "CopingNorth"),
(cope_w, "CopingWest"),
(cope_e, "CopingEast"),
]:
obj = doc.addObject("Part::Feature", name)
obj.Shape = shape
doc.recompute()
if FreeCAD.GuiUp:
FreeCAD.Gui.ActiveDocument.ActiveView.fitAll()
```
---
### Roof Eave Blocking
At the eave, the panel foam end is exposed. Solid timber blocking closes the gap and provides a nailing surface for fascia.
```python
EAVE_BLOCK_H = TOTAL_THICKNESS
eave_block = Part.makeBox(BUILDING_DEPTH, EAVE_BLOCK_H, 90,
Vector(0, 0, EAVE_HEIGHT - 90))
```
---
## MANDATORY: Panel Span Splitting
Standard SIP stock is 2440mm × 1220mm. The 1200mm width rule (side-by-side panels) is already required. The **span direction** (perpendicular to the panel seams) has the same 2440mm limit and is equally non-negotiable.
**When the rule fires:** Whenever the span dimension of any panel — roof run or any other — exceeds 2440mm.
**Non-negotiable:** A panel longer than 2440mm cannot be ordered from standard stock. The design cannot be built. Split every time, without exception.
### The Split Algorithm
```python
import math
MAX_SPAN_MM = 2440
def span_sections(total_span):
"""Return list of equal section lengths, each ≤ 2440mm."""
n = math.ceil(total_span / MAX_SPAN_MM)
section = total_span / n # float — equal sections
return [section] * n # n sections, all under 2440mm
```
Use **equal sections** (not 2440 + remainder). Equal sections avoid a tiny sliver panel and produce a cleaner, more buildable result.
### Roof Span Splice (flat roof, most common case)
Roof panels run in the SPAN direction. For a building with INNER_WIDTH > 2440mm:
```python
import math, FreeCAD, Part
from FreeCAD import Vector
# === PARAMETERS ===
BUILDING_WIDTH = 3000 # outer face to outer face (X)
PANEL_THICKNESS = 122 # wall SIP total thickness
CORE_THICKNESS = 100 # foam core depth
FACE_THICKNESS = 11 # OSB skin
ROOF_PANEL_W = 1200 # panel width (Y direction, side-by-side)
LVL_W = 45 # LVL bearer width (in span direction)
MAX_SPAN = 2440 # stock sheet max span
INNER_WIDTH = BUILDING_WIDTH - 2 * PANEL_THICKNESS # 2756mm example → 2512mm
# How many span sections?
n_span = math.ceil(INNER_WIDTH / MAX_SPAN) # 2512/2440 → ceil(1.03) = 2
SECTION_SPAN = INNER_WIDTH / n_span # 2512 / 2 = 1256mm (fits easily)
# → 2 sections of 1256mm, 1 LVL bearer between them
# Roof panel loop (width direction, Y) × span sections (X direction)
# Build in span sections first, then tile in the width direction
roof_parts = []
for i_width, pw in enumerate(panel_widths): # panel_widths = 1200mm buckets in Y
y = PANEL_THICKNESS + sum(panel_widths[:i_width])
x = PANEL_THICKNESS
for i_span in range(n_span):
# Panel section
sx = SECTION_SPAN
f1 = Part.makeBox(sx, pw, FACE_THICKNESS, Vector(x, y, ROOF_Z))
co = Part.makeBox(sx, pw, CORE_THICKNESS, Vector(x, y, ROOF_Z + FACE_THICKNESS))
f2 = Part.makeBox(sx, pw, FACE_THICKNESS, Vector(x, y, ROOF_Z + FACE_THICKNESS + CORE_THICKNESS))
roof_parts.append(f1.fuse(co).fuse(f2))
# LVL span bearer between this section and the next
if i_span < n_span - 1:
bearer = Part.makeBox(
LVL_W, pw, CORE_THICKNESS,
Vector(x + sx - LVL_W / 2, y, ROOF_Z + FACE_THICKNESS)
)
roof_parts.append(bearer)
x += sx
```
**Worked examples:**
| Building width | INNER_WIDTH | n sections | Section span | LVL bearers |
|---------------|-------------|------------|--------------|-------------|
| 2m (INNER~1756) | 1756mm | 1 | 1756mm | 0 — no split needed |
| 3m (INNER~2512) | 2512mm | 2 | 1256mm | 1 mid-span bearer |
| 4m (INNER~3756) | 3756mm | 2 | 1878mm | 1 mid-span bearer |
| 5m (INNER~4756) | 4756mm | 2 | 2378mm | 1 mid-span bearer |
| 6m (INNER~5756) | 5756mm | 3 | 1919mm | 2 bearers at 1/3 and 2/3 span |
### Mono-Pitch / Gable Roof Panel Span
The same algorithm applies. The "span" is measured along the slope (rafter direction). For a mono-pitch:
```python
SLOPE_LENGTH = BUILDING_DEPTH / math.cos(PITCH_RAD) # slant length along roof surface
n_span = math.ceil(SLOPE_LENGTH / MAX_SPAN)
section_span = SLOPE_LENGTH / n_span
```
---
## ⚠ Non-Negotiable Roof Rules
4. **Flat roofs MUST have a visible drainage fall of 1:20 and scupper outlets.**
Use `FALL_HEIGHT = INNER_DEPTH / 20` and `INSUL_MIN = 50`. Tapered insulation MUST be a polygon-face wedge (not a flat box), and the membrane on top MUST also be a matching wedge so the finished roof surface reads as sloped. Scupper openings MUST be cut through the parapet at the low side. A flat roof with no scuppers behind a parapet will flood. Never use `INSUL_MIN = 18` or `FALL_RATIO = 40`.
6. **Any panel span exceeding 2440mm MUST be split into multiple sections with LVL bearers.**
Standard SIP stock sheets are 2440mm long. Split the span into n equal sections where n = ceil(span / 2440), and model an LVL bearer (45mm wide × panel core depth) at every splice point.
9. **Gable end walls MUST include the full triangular gable section above eave height.**
For a duo-pitch roof with the ridge running along BUILDING_WIDTH (X axis), the EAST and WEST walls are gable end walls. Each gable end wall component MUST include: (a) the rectangular SIP wall section from Z=0 to Z=EAVE_HEIGHT, AND (b) a triangular prism from Z=EAVE_HEIGHT to Z=RIDGE_Z.
10. **Duo-pitch roof panels MUST use the correct placement formula.**
South slope: `Rotation(Vector(1,0,0), PITCH_DEG)`, base `Vector(0, PANEL_THICKNESS, EAVE_HEIGHT)`. North slope: `Rotation(Vector(1,0,0), 180 - PITCH_DEG)`, base `Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, EAVE_HEIGHT)`. Using `Rotation(X, -PITCH_DEG)` for north is wrong.
SIP Roof Construction
Native
Mono-pitch, duo-pitch, and flat roof construction code, ridge bevels, gable sections, and flat roof drainage. Injected to agents building roof components.
~7191 tokens
# SIP Building Decomposition Skill
This skill is used during **manifest generation and design spec creation**. It covers how to decompose a SIP building brief into a set of component agents, naming conventions, ordering rules, and coordinate system. It does not contain FreeCAD construction geometry — that is in the domain-specific skills (sip_walls, sip_roofs, etc.).
---
## When to Apply
Apply this skill when the project involves SIP construction: garden rooms, sheds, studios, cabins, or any non-habitable structure where walls and roof are built from structural insulated panels. If the brief mentions "SIP", "structural insulated panel", "wall panel", or "roof panel", this skill applies.
---
## Complete SIP Building — Required Agents
A complete SIP building manifest MUST include all of the following agent types. A manifest missing any of these is incomplete.
| Agent type | Required count | Purpose |
|---|---|---|
| `foundation` | exactly 1 | Concrete slab or strip foundation |
| `*_wall` | 4 (all perimeter walls) | South, north, east, west walls |
| `*_roof` | 1+ (depends on roof type) | Roof assembly |
| (fasteners — deferred) | — | Structural fixings (future) |
**All four perimeter walls are mandatory.** A brief that mentions only one wall means the other three must still be included. A manifest with fewer than four wall agents is wrong.
---
## ⚠ NON-NEGOTIABLE: Agent Naming
These naming rules are enforced by validators. Violating them produces validation failures.
### Foundation agent
The foundation agent MUST be named exactly:
```
"agent_id": "foundation"
```
The following names are **FORBIDDEN** — they will fail validation:
- ❌ `concrete_slab`
- ❌ `slab`
- ❌ `strip_foundation`
- ❌ `foundation_slab`
- ❌ `base_plate` or `base_plate_*`
- ❌ `foundation_front`, `foundation_south`, `foundation_north`, etc.
**There is exactly one foundation agent and its id is `foundation`.** No exceptions.
**Wrong:**
```json
{ "agent_id": "concrete_slab", "role": "foundation" }
```
**Correct:**
```json
{ "agent_id": "foundation", "role": "foundation" }
```
### Wall agents
Wall agents use directional names: `south_wall`, `north_wall`, `east_wall`, `west_wall`.
For buildings with multiple wall panels on one face, the wall agent still represents the full assembled wall — panel splitting happens inside the component, not as separate agents.
### Roof agents
Use descriptive names: `flat_roof`, `mono_pitch_roof`, `gable_roof_south`, `gable_roof_north`.
---
## Agent Dependency Ordering
Dependencies define which agents must complete before others can start. Always declare:
```json
{
"agent_id": "south_wall",
"dependencies": ["foundation.dimensions"]
}
```
Standard ordering:
1. `foundation` — no dependencies
2. All four walls — depend on `foundation.dimensions`
3. Roof — depends on all four walls (e.g. `south_wall.top_plate`, `north_wall.top_plate`)
---
## Panel Size Limits — When to Split Agents vs. Split Panels
Standard SIP stock: **2440mm × 1220mm**. The 2440mm is the span limit; 1200mm is the standard panel width.
**Panels within a single wall/roof agent are always split** — a 6m wall uses five 1200mm panels joined by splines. This splitting happens inside the component code, not as separate agents.
**Agent splitting** is only needed if the design is inherently multi-section (e.g. two roof slopes for a duo-pitch). One wall = one agent regardless of length.
---
## MANDATORY: Design Spec `"roof"` Section
Every SIP design spec **MUST** include a top-level `"roof"` object. Without it the profile validator fires a warning on every generation and downstream component agents have no authoritative roof type.
| 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 garden room:
```json
"roof": {
"roof_type_key": "duo_pitch",
"roof_structure_key": "roof_structure/sip_simple_gable_panels",
"pitch_degrees": 20,
"ridge_direction": "east_west"
}
```
**Non-negotiable:** A design spec with no `roof` section will always produce a validation warning. Include the `roof` object even when the brief does not specify a pitch angle — default to `pitch_degrees: 20` for duo-pitch, `pitch_degrees: 5` for mono-pitch, and omit `pitch_degrees` for flat.
---
## Coordinate System
All components share a common building coordinate system:
```
Origin (0, 0, 0) = SW corner of building footprint at Z = 0 (top of slab / floor level)
X-axis = East (along building width, BUILDING_WIDTH)
Y-axis = North (along building depth, BUILDING_DEPTH)
Z-axis = Up (vertical)
```
The foundation agent defines the global footprint. All wall and roof agents must use the same BUILDING_WIDTH, BUILDING_DEPTH, and PANEL_THICKNESS as the foundation.
Key shared variables declared in the manifest (all agents must agree on these):
| Variable | Where set | Consumed by |
|---|---|---|
| `BUILDING_WIDTH` | foundation | all walls, roof |
| `BUILDING_DEPTH` | foundation | all walls, roof |
| `PANEL_THICKNESS` | design spec | all walls, roof positioning |
| `WALL_HEIGHT` | wall agents | roof eave height |
| `EAVE_HEIGHT` | derived | roof slopes |
SIP Decomposition
Native
Agent naming conventions, dependency ordering, coordinate system, and decomposition rules for SIP building manifests. Injected at manifest_generation and design_spec_generate.
~1355 tokens
# SIP Foundation Construction Skill
## Foundations
### 1. Concrete Slab (most common for SIP)
**The slab MUST extend beyond the wall footprint on all sides.** Use `SLAB_OVERHANG = 200mm` minimum. The wall bottom plates sit on the slab surface; the slab overhang provides bearing for the edge beam and prevents the wall foam from being at the slab edge.
```
Section view:
┌──────────────────────────────────────────────┐
│ SIP wall panel (foam min 150mm above ground)│
├──────────────────────────────────────────────┤ ← PT bottom plate (45×90mm)
├──────────────────────────────────────────────┤ ← DPC membrane (3mm)
├──────────────────────────────────────────────┤ ← slab top (Z=0)
│ concrete slab 125mm │
├─────┬────────────────────────────────┬───────┤
│edge │ insulation (optional) │ edge │
│beam │ │ beam │
│300× │ │ 300× │
│600 │ │ 600 │
└─────┴────────────────────────────────┴───────┘
← SLAB_OVERHANG (200mm min) →
← slab extends past wall face on all sides
```
```python
# === SLAB PARAMETERS ===
BUILDING_WIDTH = 3000 # outer wall face to outer wall face
BUILDING_DEPTH = 2000
SLAB_OVERHANG = 200 # slab extends beyond wall footprint — minimum 200mm
SLAB_W = BUILDING_WIDTH + 2 * SLAB_OVERHANG
SLAB_D = BUILDING_DEPTH + 2 * SLAB_OVERHANG
SLAB_T = 125 # slab field thickness
EDGE_BEAM_W = 300 # thickened perimeter beam width
EDGE_BEAM_D = 600 # total depth including slab thickness
DPC_T = 3 # damp proof course
CORE_THICKNESS = 150 # foam core of the SIP panel (SIP-150)
FACE_THICKNESS = 11 # OSB skin each side
SILL_PLATE_W = CORE_THICKNESS # plate slots into foam groove — NOT full SIP thickness
SILL_PLATE_H = 90
# Slab origin is SW corner of slab at slab bottom surface
# Z=0 = slab top (all wall and floor references start here)
slab_x0 = -SLAB_OVERHANG
slab_y0 = -SLAB_OVERHANG
# Slab field
slab = Part.makeBox(SLAB_W, SLAB_D, SLAB_T,
Vector(slab_x0, slab_y0, -SLAB_T))
# Edge beams (perimeter thickening)
eb_n = Part.makeBox(SLAB_W, EDGE_BEAM_W, EDGE_BEAM_D,
Vector(slab_x0, slab_y0 + SLAB_D - EDGE_BEAM_W, -EDGE_BEAM_D))
eb_s = Part.makeBox(SLAB_W, EDGE_BEAM_W, EDGE_BEAM_D,
Vector(slab_x0, slab_y0, -EDGE_BEAM_D))
eb_e = Part.makeBox(EDGE_BEAM_W, SLAB_D, EDGE_BEAM_D,
Vector(slab_x0 + SLAB_W - EDGE_BEAM_W, slab_y0, -EDGE_BEAM_D))
eb_w = Part.makeBox(EDGE_BEAM_W, SLAB_D, EDGE_BEAM_D,
Vector(slab_x0, slab_y0, -EDGE_BEAM_D))
foundation = slab.fuse(eb_n).fuse(eb_s).fuse(eb_e).fuse(eb_w)
# Anchor bolts M12 at 600mm centres (perimeter, modelled as cylinders)
BOLT_D = 12
BOLT_L = 150
for x_pos in range(300, BUILDING_WIDTH - 300, 600):
# South edge — bolt centred in foam channel (Y = FACE_THICKNESS + SILL_PLATE_W / 2)
bolt = Part.makeCylinder(BOLT_D / 2, BOLT_L,
Vector(x_pos, FACE_THICKNESS + SILL_PLATE_W / 2, -BOLT_L))
foundation = foundation.fuse(bolt)
# DPC membrane strip and sill plate sit at Z=0 on slab surface.
# Offset by FACE_THICKNESS so they align with the foam channel (OSB skins overhang on both sides).
dpc_s = Part.makeBox(BUILDING_WIDTH, SILL_PLATE_W, DPC_T,
Vector(0, FACE_THICKNESS, 0))
sp_s = Part.makeBox(BUILDING_WIDTH, SILL_PLATE_W, SILL_PLATE_H,
Vector(0, FACE_THICKNESS, DPC_T))
# Repeat for N, E, W walls
```
**Key rules for slab foundations:**
- Slab top surface = Z=0 (all wall/floor heights reference from this)
- `SLAB_OVERHANG` ≥ 200mm — if SLAB_W or SLAB_D equals BUILDING_WIDTH/DEPTH, the slab is too small
- Foam core bottom = DPC_T + SILL_PLATE_H = must be ≥ 150mm above finished ground (≈ top of edge beam)
- Anchor bolts: M12, 150mm embedment, 600mm centres, 150mm from corners
---
### 2. Strip Foundation
A single perimeter concrete strip foundation — a flat rectangular ring/frame sitting directly under all sole plates. Modelled as **one FreeCAD component** with agent id `foundation`. No T-shaped cross-section, no separate foundation wall segment, no individual side agents.
**NON-NEGOTIABLE: Strip foundations are ONE component, ONE agent (`foundation`), ONE FreeCAD model. Do NOT create four separate agents (`foundation_strip_south` etc.). Do NOT add a foundation wall box on top of the strip. The strip top face IS the sole plate bearing face at Z=0.**
**Z reference:** Z=0 is the top face of the strip (sole plate bearing face). Strip extends downward to Z=−STRIP_D.
**Plan geometry:** The strip is a hollow rectangular frame. Build it by fusing four flat boxes:
- South segment: full outer width × STRIP_W, placed at south edge
- North segment: full outer width × STRIP_W, placed at north edge
- West segment: STRIP_W wide × inner depth (between south and north segments)
- East segment: STRIP_W wide × inner depth (between south and north segments)
**Formula:**
- Outer width = `BUILDING_WIDTH + 2 × STRIP_W`
- Outer depth = `BUILDING_DEPTH + 2 × STRIP_W`
- Inner void starts at Y=STRIP_W (south), ends at Y=STRIP_W+BUILDING_DEPTH (north)
- All four segments are STRIP_D tall; top at Z=0, bottom at Z=−STRIP_D
```python
# === STRIP FOUNDATION PARAMETERS ===
BUILDING_WIDTH = 4000 # outer wall face to outer wall face (from spec)
BUILDING_DEPTH = 3000
STRIP_W = 450 # strip width (plan dimension, perpendicular to wall)
STRIP_D = 300 # strip depth (total buried concrete thickness)
# Z=0 = top of strip = sole plate bearing face
# Strip runs from Z=0 down to Z=-STRIP_D (flat, no foundation wall above it)
# South segment — full outer width, south edge
seg_s = Part.makeBox(BUILDING_WIDTH + 2 * STRIP_W, STRIP_W, STRIP_D,
Vector(-STRIP_W, -STRIP_W, -STRIP_D))
# North segment — full outer width, north edge
seg_n = Part.makeBox(BUILDING_WIDTH + 2 * STRIP_W, STRIP_W, STRIP_D,
Vector(-STRIP_W, BUILDING_DEPTH, -STRIP_D))
# West segment — between south and north, west edge
seg_w = Part.makeBox(STRIP_W, BUILDING_DEPTH, STRIP_D,
Vector(-STRIP_W, 0, -STRIP_D))
# East segment — between south and north, east edge
seg_e = Part.makeBox(STRIP_W, BUILDING_DEPTH, STRIP_D,
Vector(BUILDING_WIDTH, 0, -STRIP_D))
# Fuse into single perimeter ring
foundation = seg_s.fuse(seg_n).fuse(seg_w).fuse(seg_e)
feature = doc.addObject("Part::Feature", "StripFoundation")
feature.Shape = foundation
```
**Key rules for strip foundations:**
- ONE agent (`foundation`), ONE model — never split into four side agents
- No foundation wall segment — the strip top face at Z=0 is the bearing face for sole plates
- South and north segments span `BUILDING_WIDTH + 2 × STRIP_W`; east and west span `BUILDING_DEPTH`
- All four segments are the same depth (STRIP_D); top face exactly at Z=0
- Cladding overhangs the foundation face by 40–50mm — the strip exterior face is flush with the SIP wall exterior face
---
### 3. Screw Pile Foundation
Steel screw piles driven to bearing depth, carrying LVL bearer beams. Best for sloped sites and remote/off-grid builds.
```python
# === SCREW PILE PARAMETERS ===
PILE_DIAMETER = 114
PILE_SPACING_X = 2400
PILE_SPACING_Y = 2400
PILE_EXPOSED_H = 600
BEARER_W = 90
BEARER_H = 190
piles_x = (BUILDING_WIDTH // PILE_SPACING_X) + 1
piles_y = (BUILDING_DEPTH // PILE_SPACING_Y) + 1
parts = []
for ix in range(piles_x):
for iy in range(piles_y):
x = ix * PILE_SPACING_X
y = iy * PILE_SPACING_Y
pile = Part.makeCylinder(PILE_DIAMETER / 2, PILE_EXPOSED_H,
Vector(x, y, -PILE_EXPOSED_H))
parts.append(pile)
# Bearer beams spanning across building width
for iy in range(piles_y):
y = iy * PILE_SPACING_Y
b1 = Part.makeBox(BUILDING_WIDTH, BEARER_W, BEARER_H, Vector(0, y, 0))
b2 = Part.makeBox(BUILDING_WIDTH, BEARER_W, BEARER_H,
Vector(0, y + BEARER_W + 10, 0))
parts.extend([b1, b2])
```
---
## Building Assembly — Coordinate System (Slab and Footprint)
```
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
```
## 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, BUILDINGWIDTHxBUILDINGDEPTHmm 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`.
## ⚠ Non-Negotiable Foundation Rule
5. **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.
SIP Foundation Construction
Native
Concrete slab and strip foundation geometry, DPC placement, slab overhang rule, and the NON-NEGOTIABLE foundation agent naming rule.
~3099 tokens
# SIP Openings Construction Skill
## 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 |
---
## ⚠ Non-Negotiable Opening Rule
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.
SIP Openings Construction
Native
Full-depth timber buck construction for windows and doors, framing member sizing, panel zone helper, and multiple-opening layout patterns.
~3596 tokens
# SIP Interface Rules Skill
This skill describes the **static connection rules** between SIP component types. Every agent that interfaces with another component receives this skill. Use it to know which side of a connection you are responsible for, and what dimensions to use.
Dimensional values are declared in the `connection_details` catalog injected into this stage. Reference catalog entries by key rather than hardcoding values.
> **Note on dynamic dimensions:** This skill covers interface *rules* (who cuts what, where). The *actual dimensions* of adjacent components (e.g. the exact thickness of the roof panel chosen for this project) are resolved at manifest time and passed via the agent's `domain_knowledge` and `interfaces_provided` fields. Use those values when available; fall back to catalog defaults when not.
---
## Roof-to-Wall Interface
**Rule:** The wall agent is responsible for the connection geometry at its top edge. The roof agent bears on the wall top plate and does not modify the wall.
**Wall agent responsibilities:**
- The double top plate (2× 45×90mm timber) provides the bearing surface for the roof panel.
- For pitched roofs: the top plate must be cut at the pitch angle so the roof panel bears flat. Model this as an angled-cut top plate or use a separate angled bearer.
- For flat roofs: the top plate is horizontal; the roof panel bears flat on it with no cuts needed.
- The wall top plate depth is `CORE_THICKNESS` (not `TOTAL_THICKNESS`) — it slots into the foam groove.
**Roof agent responsibilities:**
- The roof panel eave end bears on the wall top plate.
- For pitched roofs: cut the eave bevel (`eave_bevel_y = TOTAL_THICKNESS / tan(PITCH_RAD)`) so the panel bears cleanly.
- For flat roofs: the panel bottom face sits directly on the top plate — no cuts needed.
**Shared dimension:**
- `PANEL_THICKNESS` (wall SIP total thickness) defines the inset for roof panel positioning:
- Mono-pitch: `Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot)`
- Duo-pitch south: `Placement(Vector(0, PANEL_THICKNESS, EAVE_HEIGHT), rot)`
- Duo-pitch north: `Placement(Vector(0, BUILDING_DEPTH - PANEL_THICKNESS, EAVE_HEIGHT), rot)`
---
## Wall-to-Foundation Interface
**Rule:** The wall agent is responsible for the sole plate (bottom plate) at Z=0. The foundation agent is responsible for the slab surface and DPC layer.
**Wall agent responsibilities:**
- Create a 45×90mm PT timber bottom plate at Z=0 as the first solid.
- Plate depth = `CORE_THICKNESS` (not `TOTAL_THICKNESS`) — it slots into the foam groove.
- Y-offset = `FACE_THICKNESS` so OSB skins overhang on both faces.
- SIP panels start at Z=90 (top of bottom plate).
**Foundation agent responsibilities:**
- The slab top surface is at Z=0.
- A DPC (damp proof course) layer sits between slab and bottom plate — model as a 3mm high-density polyethylene layer if required, or omit for clarity.
- The slab extends `SLAB_OVERHANG` (minimum 200mm) beyond the wall footprint on all sides.
**Connection detail:** `sole_plate_to_slab` in the `connection_details` catalog.
---
## Wall-to-Wall Corner Interface
**Rule:** For L-corners, one wall runs full length to the outer face; the perpendicular wall butts against it. A double block spline fills the internal corner pocket.
**Responsibility split:**
- The "full-length" wall includes the corner panel at its end — no special treatment needed.
- The "butting" wall's end panel butts against the exterior OSB face of the full-length wall. Its bottom plate runs to the face of the full-length wall. A double block spline (2× 45×90mm) fills the corner pocket on the butting wall's end.
**Corner spline geometry:**
```python
# Double block spline at L-corner (butting wall end)
# Both splines sit in the foam channel, side by side
spline_a = Part.makeBox(45, 90, PANEL_HEIGHT,
Vector(x_corner, (TOTAL_THICKNESS - 90) / 2, BOTTOM_PLATE_H))
spline_b = Part.makeBox(45, 90, PANEL_HEIGHT,
Vector(x_corner + 45, (TOTAL_THICKNESS - 90) / 2, BOTTOM_PLATE_H))
```
---
## Opening-to-Wall Interface
**Rule:** Openings (windows, doors) are framed within the wall component. The wall agent is responsible for stopping SIP panels at the king stud face and building the full-depth timber buck. If a window or door is described in the wall brief, the wall component includes all framing — there is no separate framing agent.
**King stud depth:** `KING_D = TOTAL_THICKNESS` (full wall thickness, not 90mm).
**Opening agent (if separate):** A separate opening agent adds only the glazed unit or door leaf — it does NOT build framing. Its X position, width, and height come from the wall agent's `interfaces_provided`.
---
## Quick Interface Checklist
Before generating any component that touches another, confirm:
| Check | Wall | Roof | Foundation | Opening |
|---|---|---|---|---|
| Sole plate at Z=0 with CORE_THICKNESS depth | ✅ my responsibility | — | — | — |
| Top plate angled for pitched roof | ✅ my responsibility | — | — | — |
| Eave bevel cut | — | ✅ my responsibility | — | — |
| Slab extends SLAB_OVERHANG beyond walls | — | — | ✅ my responsibility | — |
| King studs are TOTAL_THICKNESS deep | ✅ (if I have openings) | — | — | ✅ |
| Corner spline on butting wall end | ✅ (butting wall only) | — | — | — |
SIP Interface Rules
Native
Static connection rules between SIP components: roof-to-wall, wall-to-foundation, wall-to-wall corners, and opening-to-wall. Injected alongside domain skills.
~1331 tokens
# FreeCAD Direct Modeling Skill
This skill defines how to generate reliable FreeCAD code using **direct modeling** with the Part workbench.
Direct modeling is simpler, more reliable, and better suited for AI-generated CAD code.
## Why Direct Modeling?
| Aspect | Direct (Part) | Parametric (PartDesign) |
|--------|---------------|-------------------------|
| **Reliability** | High - no face references | Low - face refs are unstable |
| **Code complexity** | Simple | Complex (sketches, constraints) |
| **Error rate** | Low | High |
| **Editable in FreeCAD** | No (final shape only) | Yes (feature tree) |
| **Best for** | AI generation | Manual CAD work |
---
## Standard Code Structure
```python
import FreeCAD
import Part
import math
from FreeCAD import Vector, Placement, Rotation
# === PARAMETERS ===
LENGTH = 100
WIDTH = 60
HEIGHT = 40
HOLE_DIAMETER = 10
# === DOCUMENT ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Model")
# === BUILD GEOMETRY ===
# Start with a base shape
base = Part.makeBox(LENGTH, WIDTH, HEIGHT)
# Add features using boolean operations
hole = Part.makeCylinder(HOLE_DIAMETER / 2, HEIGHT, Vector(LENGTH/2, WIDTH/2, 0))
result = base.cut(hole)
# === ADD TO DOCUMENT ===
feature = doc.addObject("Part::Feature", "Model")
feature.Shape = result
doc.recompute()
if FreeCAD.GuiUp:
FreeCAD.Gui.ActiveDocument.ActiveView.fitAll()
```
---
## Primitive Shapes
### Box
```python
# Part.makeBox(length, width, height, position, direction)
box = Part.makeBox(100, 60, 40)
# Centered at origin
box = Part.makeBox(100, 60, 40, Vector(-50, -30, 0))
# With custom placement
box = Part.makeBox(100, 60, 40)
box.translate(Vector(10, 20, 30))
```
### Cylinder
```python
# Part.makeCylinder(radius, height, position, direction)
cyl = Part.makeCylinder(25, 50) # At origin, along Z
# Horizontal cylinder (along X)
cyl = Part.makeCylinder(25, 100, Vector(0, 0, 0), Vector(1, 0, 0))
# Positioned
cyl = Part.makeCylinder(10, 30, Vector(50, 30, 0), Vector(0, 0, 1))
```
### Sphere
```python
# Part.makeSphere(radius, center)
sphere = Part.makeSphere(25)
sphere = Part.makeSphere(25, Vector(50, 50, 50))
```
### Cone
```python
# Part.makeCone(radius1, radius2, height, position, direction)
cone = Part.makeCone(30, 10, 50) # Tapers from r=30 to r=10
cone = Part.makeCone(20, 0, 40) # Pointed cone
```
### Torus
```python
# Part.makeTorus(majorRadius, minorRadius)
torus = Part.makeTorus(50, 10) # Donut shape
```
---
## Boolean Operations
### Fusion (Union/Add)
```python
# Combine two shapes
box = Part.makeBox(100, 100, 50)
cyl = Part.makeCylinder(30, 80, Vector(50, 50, 0))
result = box.fuse(cyl)
# Multiple shapes
shape1.fuse([shape2, shape3, shape4])
```
### Cut (Subtract)
```python
# Remove material
base = Part.makeBox(100, 100, 50)
hole = Part.makeCylinder(15, 50, Vector(50, 50, 0))
result = base.cut(hole)
# Multiple cuts
base.cut([hole1, hole2, hole3])
```
### Common (Intersection)
```python
# Keep only overlapping volume
box = Part.makeBox(100, 100, 100)
sphere = Part.makeSphere(60, Vector(50, 50, 50))
result = box.common(sphere) # Rounded box corner
```
---
## Transformations
### Translate (Move)
```python
shape = Part.makeBox(50, 50, 50)
shape.translate(Vector(100, 0, 0)) # Move 100mm along X
```
### Rotate
```python
import math
shape = Part.makeBox(50, 50, 50)
# Rotate around Z axis at origin
shape.rotate(Vector(0, 0, 0), Vector(0, 0, 1), 45) # 45 degrees
# Rotate around center
center = Vector(25, 25, 25)
shape.rotate(center, Vector(0, 0, 1), 90)
```
### Mirror
```python
# Mirror across XY plane at Z=0
shape = Part.makeBox(50, 50, 50, Vector(10, 10, 10))
mirrored = shape.mirror(Vector(0, 0, 0), Vector(0, 0, 1))
```
### Scale
```python
# Scale uniformly
import FreeCAD
matrix = FreeCAD.Matrix()
matrix.scale(2, 2, 2) # Double size
scaled = shape.transformGeometry(matrix)
# Non-uniform scale
matrix.scale(2, 1, 0.5) # Stretch X, compress Z
```
---
## Complex Shapes
### Extrusion
```python
# Create a wire (closed profile)
wire = Part.makePolygon([
Vector(0, 0, 0),
Vector(100, 0, 0),
Vector(100, 50, 0),
Vector(50, 50, 0),
Vector(50, 30, 0),
Vector(0, 30, 0),
Vector(0, 0, 0) # Close the profile
])
face = Part.Face(wire)
solid = face.extrude(Vector(0, 0, 40)) # Extrude 40mm in Z
```
### Revolution
```python
# Profile to revolve (in XZ plane)
profile = Part.makePolygon([
Vector(10, 0, 0),
Vector(30, 0, 0),
Vector(30, 0, 50),
Vector(20, 0, 60),
Vector(10, 0, 50),
Vector(10, 0, 0)
])
face = Part.Face(Part.Wire(profile))
# Revolve around Z axis
solid = face.revolve(Vector(0, 0, 0), Vector(0, 0, 1), 360)
```
### Loft
```python
# Multiple profiles at different heights
def make_circle_wire(center, radius):
circle = Part.Circle(center, Vector(0, 0, 1), radius)
return Part.Wire(circle.toShape())
profiles = [
make_circle_wire(Vector(0, 0, 0), 30),
make_circle_wire(Vector(0, 0, 50), 40),
make_circle_wire(Vector(0, 0, 100), 20),
]
solid = Part.makeLoft(profiles, True) # True = solid
```
### Sweep
```python
# Sweep a profile along a path
profile = Part.makeCircle(10)
profile = Part.Wire(profile)
path = Part.makeLine(Vector(0, 0, 0), Vector(100, 50, 30))
path = Part.Wire(path)
solid = Part.makePipe(path, profile)
```
---
## Fillets and Chamfers
### Fillet (Round edges)
```python
box = Part.makeBox(100, 60, 40)
# Fillet specific edges by index
filleted = box.makeFillet(5, [box.Edges[0], box.Edges[4], box.Edges[8]])
# Fillet all edges
filleted = box.makeFillet(3, box.Edges)
```
### Chamfer
```python
box = Part.makeBox(100, 60, 40)
chamfered = box.makeChamfer(3, box.Edges)
```
---
## Shell (Hollow out)
```python
box = Part.makeBox(100, 60, 40)
# Shell with 2mm wall thickness, removing top face
# Faces are indexed; find top face by checking normals
top_face = None
for face in box.Faces:
if face.normalAt(0, 0).z > 0.9:
top_face = face
break
shelled = box.makeShell([top_face], 2) # 2mm walls
```
---
## Pattern (Arrays)
### Linear Array
```python
base = Part.makeCylinder(5, 10)
shapes = [base]
for i in range(1, 5):
copy = base.copy()
copy.translate(Vector(i * 20, 0, 0))
shapes.append(copy)
result = shapes[0].fuse(shapes[1:])
```
### Polar Array
```python
import math
base = Part.makeCylinder(5, 10, Vector(30, 0, 0))
shapes = [base]
for i in range(1, 8):
angle = i * 360 / 8
copy = base.copy()
copy.rotate(Vector(0, 0, 0), Vector(0, 0, 1), angle)
shapes.append(copy)
result = shapes[0].fuse(shapes[1:])
```
---
## Complete Examples
### Bracket with Holes
```python
import FreeCAD
import Part
from FreeCAD import Vector
# Parameters
BRACKET_L = 80
BRACKET_W = 40
BRACKET_H = 5
HOLE_DIA = 8
HOLE_SPACING = 30
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Bracket")
# Base plate
plate = Part.makeBox(BRACKET_L, BRACKET_W, BRACKET_H)
# Mounting holes
holes = []
for x in [BRACKET_L/2 - HOLE_SPACING/2, BRACKET_L/2 + HOLE_SPACING/2]:
hole = Part.makeCylinder(HOLE_DIA/2, BRACKET_H, Vector(x, BRACKET_W/2, 0))
holes.append(hole)
# Cut holes
result = plate.cut(holes)
# Add to document
feature = doc.addObject("Part::Feature", "Bracket")
feature.Shape = result
doc.recompute()
```
### Cup/Container
```python
import FreeCAD
import Part
from FreeCAD import Vector
# Parameters
OUTER_DIA = 80
INNER_DIA = 74
HEIGHT = 100
WALL_THICK = 3
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Cup")
# Outer cylinder
outer = Part.makeCylinder(OUTER_DIA/2, HEIGHT)
# Inner cavity (leave bottom thickness)
inner = Part.makeCylinder(INNER_DIA/2, HEIGHT - WALL_THICK, Vector(0, 0, WALL_THICK))
# Cut to create hollow
result = outer.cut(inner)
feature = doc.addObject("Part::Feature", "Cup")
feature.Shape = result
doc.recompute()
```
### Gear (Simplified)
```python
import FreeCAD
import Part
import math
from FreeCAD import Vector
# Parameters
NUM_TEETH = 20
MODULE = 2
THICKNESS = 10
BORE_DIA = 10
pitch_dia = NUM_TEETH * MODULE
outer_dia = pitch_dia + 2 * MODULE
root_dia = pitch_dia - 2.5 * MODULE
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Gear")
# Base cylinder
gear = Part.makeCylinder(outer_dia/2, THICKNESS)
# Cut tooth gaps
tooth_angle = 360 / NUM_TEETH
gap_angle = tooth_angle * 0.4
for i in range(NUM_TEETH):
angle = i * tooth_angle
# Create wedge-shaped gap
gap = Part.makeBox(outer_dia, outer_dia/4, THICKNESS)
gap.translate(Vector(root_dia/2, -outer_dia/8, 0))
gap.rotate(Vector(0, 0, 0), Vector(0, 0, 1), angle)
gear = gear.cut(gap)
# Center bore
bore = Part.makeCylinder(BORE_DIA/2, THICKNESS)
gear = gear.cut(bore)
feature = doc.addObject("Part::Feature", "Gear")
feature.Shape = gear
doc.recompute()
```
---
## Rules (Non-Negotiable)
### ALWAYS Do:
1. **Import math** for trigonometry: `import math`
2. **Use Vector() with positional args**: `Vector(10, 20, 30)`
3. **Call doc.recompute()** at the end
4. **Define parameters at the top** for easy modification
### NEVER Do:
1. ❌ `Vector(x=10, y=20)` - No keyword arguments
2. ❌ Zero-dimension shapes - `Part.makeBox(0, 50, 50)` fails
3. ❌ Face references - Not needed in direct modeling
4. ❌ `PartDesign::Body` - Use `Part::Feature` for direct modeling
### Output Format
When generating code:
1. Keep text response to 1-2 sentences
2. Put complete code in a ```python block at the END
3. Do NOT repeat or explain the code in text
FreeCAD Direct Modeling
Native
Part workbench primitives, booleans, transformations, and extrusions. Default skill injected at every stage that produces FreeCAD Python code.
~2361 tokens
The Blender Rendering (Core)
skill is always injected for every render. Add complementary skills below to guide style on top of it.
Blender Rendering (Core)
Native
Always loaded
Always injected for every render. Defines camera rules, lighting, ColorRamp material patterns, and render type dispatch. Read-only.
Concrete slab and strip foundation geometry, DPC placement, slab overhang rule, and the NON-NEGOTIABLE foundation agent naming rule.
/skills/sip_foundations.md
Estimated tokens
3099
Characters
12394
Source
Native
Markdown
# SIP Foundation Construction Skill
## Foundations
### 1. Concrete Slab (most common for SIP)
**The slab MUST extend beyond the wall footprint on all sides.** Use `SLAB_OVERHANG = 200mm` minimum. The wall bottom plates sit on the slab surface; the slab overhang provides bearing for the edge beam and prevents the wall foam from being at the slab edge.
```
Section view:
┌──────────────────────────────────────────────┐
│ SIP wall panel (foam min 150mm above ground)│
├──────────────────────────────────────────────┤ ← PT bottom plate (45×90mm)
├──────────────────────────────────────────────┤ ← DPC membrane (3mm)
├──────────────────────────────────────────────┤ ← slab top (Z=0)
│ concrete slab 125mm │
├─────┬────────────────────────────────┬───────┤
│edge │ insulation (optional) │ edge │
│beam │ │ beam │
│300× │ │ 300× │
│600 │ │ 600 │
└─────┴────────────────────────────────┴───────┘
← SLAB_OVERHANG (200mm min) →
← slab extends past wall face on all sides
```
```python
# === SLAB PARAMETERS ===
BUILDING_WIDTH = 3000 # outer wall face to outer wall face
BUILDING_DEPTH = 2000
SLAB_OVERHANG = 200 # slab extends beyond wall footprint — minimum 200mm
SLAB_W = BUILDING_WIDTH + 2 * SLAB_OVERHANG
SLAB_D = BUILDING_DEPTH + 2 * SLAB_OVERHANG
SLAB_T = 125 # slab field thickness
EDGE_BEAM_W = 300 # thickened perimeter beam width
EDGE_BEAM_D = 600 # total depth including slab thickness
DPC_T = 3 # damp proof course
CORE_THICKNESS = 150 # foam core of the SIP panel (SIP-150)
FACE_THICKNESS = 11 # OSB skin each side
SILL_PLATE_W = CORE_THICKNESS # plate slots into foam groove — NOT full SIP thickness
SILL_PLATE_H = 90
# Slab origin is SW corner of slab at slab bottom surface
# Z=0 = slab top (all wall and floor references start here)
slab_x0 = -SLAB_OVERHANG
slab_y0 = -SLAB_OVERHANG
# Slab field
slab = Part.makeBox(SLAB_W, SLAB_D, SLAB_T,
Vector(slab_x0, slab_y0, -SLAB_T))
# Edge beams (perimeter thickening)
eb_n = Part.makeBox(SLAB_W, EDGE_BEAM_W, EDGE_BEAM_D,
Vector(slab_x0, slab_y0 + SLAB_D - EDGE_BEAM_W, -EDGE_BEAM_D))
eb_s = Part.makeBox(SLAB_W, EDGE_BEAM_W, EDGE_BEAM_D,
Vector(slab_x0, slab_y0, -EDGE_BEAM_D))
eb_e = Part.makeBox(EDGE_BEAM_W, SLAB_D, EDGE_BEAM_D,
Vector(slab_x0 + SLAB_W - EDGE_BEAM_W, slab_y0, -EDGE_BEAM_D))
eb_w = Part.makeBox(EDGE_BEAM_W, SLAB_D, EDGE_BEAM_D,
Vector(slab_x0, slab_y0, -EDGE_BEAM_D))
foundation = slab.fuse(eb_n).fuse(eb_s).fuse(eb_e).fuse(eb_w)
# Anchor bolts M12 at 600mm centres (perimeter, modelled as cylinders)
BOLT_D = 12
BOLT_L = 150
for x_pos in range(300, BUILDING_WIDTH - 300, 600):
# South edge — bolt centred in foam channel (Y = FACE_THICKNESS + SILL_PLATE_W / 2)
bolt = Part.makeCylinder(BOLT_D / 2, BOLT_L,
Vector(x_pos, FACE_THICKNESS + SILL_PLATE_W / 2, -BOLT_L))
foundation = foundation.fuse(bolt)
# DPC membrane strip and sill plate sit at Z=0 on slab surface.
# Offset by FACE_THICKNESS so they align with the foam channel (OSB skins overhang on both sides).
dpc_s = Part.makeBox(BUILDING_WIDTH, SILL_PLATE_W, DPC_T,
Vector(0, FACE_THICKNESS, 0))
sp_s = Part.makeBox(BUILDING_WIDTH, SILL_PLATE_W, SILL_PLATE_H,
Vector(0, FACE_THICKNESS, DPC_T))
# Repeat for N, E, W walls
```
**Key rules for slab foundations:**
- Slab top surface = Z=0 (all wall/floor heights reference from this)
- `SLAB_OVERHANG` ≥ 200mm — if SLAB_W or SLAB_D equals BUILDING_WIDTH/DEPTH, the slab is too small
- Foam core bottom = DPC_T + SILL_PLATE_H = must be ≥ 150mm above finished ground (≈ top of edge beam)
- Anchor bolts: M12, 150mm embedment, 600mm centres, 150mm from corners
---
### 2. Strip Foundation
A single perimeter concrete strip foundation — a flat rectangular ring/frame sitting directly under all sole plates. Modelled as **one FreeCAD component** with agent id `foundation`. No T-shaped cross-section, no separate foundation wall segment, no individual side agents.
**NON-NEGOTIABLE: Strip foundations are ONE component, ONE agent (`foundation`), ONE FreeCAD model. Do NOT create four separate agents (`foundation_strip_south` etc.). Do NOT add a foundation wall box on top of the strip. The strip top face IS the sole plate bearing face at Z=0.**
**Z reference:** Z=0 is the top face of the strip (sole plate bearing face). Strip extends downward to Z=−STRIP_D.
**Plan geometry:** The strip is a hollow rectangular frame. Build it by fusing four flat boxes:
- South segment: full outer width × STRIP_W, placed at south edge
- North segment: full outer width × STRIP_W, placed at north edge
- West segment: STRIP_W wide × inner depth (between south and north segments)
- East segment: STRIP_W wide × inner depth (between south and north segments)
**Formula:**
- Outer width = `BUILDING_WIDTH + 2 × STRIP_W`
- Outer depth = `BUILDING_DEPTH + 2 × STRIP_W`
- Inner void starts at Y=STRIP_W (south), ends at Y=STRIP_W+BUILDING_DEPTH (north)
- All four segments are STRIP_D tall; top at Z=0, bottom at Z=−STRIP_D
```python
# === STRIP FOUNDATION PARAMETERS ===
BUILDING_WIDTH = 4000 # outer wall face to outer wall face (from spec)
BUILDING_DEPTH = 3000
STRIP_W = 450 # strip width (plan dimension, perpendicular to wall)
STRIP_D = 300 # strip depth (total buried concrete thickness)
# Z=0 = top of strip = sole plate bearing face
# Strip runs from Z=0 down to Z=-STRIP_D (flat, no foundation wall above it)
# South segment — full outer width, south edge
seg_s = Part.makeBox(BUILDING_WIDTH + 2 * STRIP_W, STRIP_W, STRIP_D,
Vector(-STRIP_W, -STRIP_W, -STRIP_D))
# North segment — full outer width, north edge
seg_n = Part.makeBox(BUILDING_WIDTH + 2 * STRIP_W, STRIP_W, STRIP_D,
Vector(-STRIP_W, BUILDING_DEPTH, -STRIP_D))
# West segment — between south and north, west edge
seg_w = Part.makeBox(STRIP_W, BUILDING_DEPTH, STRIP_D,
Vector(-STRIP_W, 0, -STRIP_D))
# East segment — between south and north, east edge
seg_e = Part.makeBox(STRIP_W, BUILDING_DEPTH, STRIP_D,
Vector(BUILDING_WIDTH, 0, -STRIP_D))
# Fuse into single perimeter ring
foundation = seg_s.fuse(seg_n).fuse(seg_w).fuse(seg_e)
feature = doc.addObject("Part::Feature", "StripFoundation")
feature.Shape = foundation
```
**Key rules for strip foundations:**
- ONE agent (`foundation`), ONE model — never split into four side agents
- No foundation wall segment — the strip top face at Z=0 is the bearing face for sole plates
- South and north segments span `BUILDING_WIDTH + 2 × STRIP_W`; east and west span `BUILDING_DEPTH`
- All four segments are the same depth (STRIP_D); top face exactly at Z=0
- Cladding overhangs the foundation face by 40–50mm — the strip exterior face is flush with the SIP wall exterior face
---
### 3. Screw Pile Foundation
Steel screw piles driven to bearing depth, carrying LVL bearer beams. Best for sloped sites and remote/off-grid builds.
```python
# === SCREW PILE PARAMETERS ===
PILE_DIAMETER = 114
PILE_SPACING_X = 2400
PILE_SPACING_Y = 2400
PILE_EXPOSED_H = 600
BEARER_W = 90
BEARER_H = 190
piles_x = (BUILDING_WIDTH // PILE_SPACING_X) + 1
piles_y = (BUILDING_DEPTH // PILE_SPACING_Y) + 1
parts = []
for ix in range(piles_x):
for iy in range(piles_y):
x = ix * PILE_SPACING_X
y = iy * PILE_SPACING_Y
pile = Part.makeCylinder(PILE_DIAMETER / 2, PILE_EXPOSED_H,
Vector(x, y, -PILE_EXPOSED_H))
parts.append(pile)
# Bearer beams spanning across building width
for iy in range(piles_y):
y = iy * PILE_SPACING_Y
b1 = Part.makeBox(BUILDING_WIDTH, BEARER_W, BEARER_H, Vector(0, y, 0))
b2 = Part.makeBox(BUILDING_WIDTH, BEARER_W, BEARER_H,
Vector(0, y + BEARER_W + 10, 0))
parts.extend([b1, b2])
```
---
## Building Assembly — Coordinate System (Slab and Footprint)
```
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
```
## 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, BUILDINGWIDTHxBUILDINGDEPTHmm 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`.
## ⚠ Non-Negotiable Foundation Rule
5. **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.