← Skills

SIP Wall Panels (ElixiFree API)

Native Read-only

API reference for elixifree.sip wall panel functions

/skills/sip_walls_elixifree.md

Estimated tokens
4358
Characters
17429
Source
Native

Markdown

# SIP Wall Panel Skill — ElixiFree Builder API

Use the declarative builder API for all SIP components. The builder captures
your intent as parameters — no geometry code, no raw Part calls for standard components.

## Import

**Exact exported names — use these exactly, no variations:**

```python
from elixifree.domains.sip import Wall, RoofPanel, PitchedRoofSlope, FloorDeck, Foundation, sip_constants
```

- Wall panels → `Wall` (NOT `WallPanel`, `SIPWall`, `Panel`, or any other name)
- Flat roof panels → `RoofPanel` (flat roofs ONLY)
- Pitched roof slopes → `PitchedRoofSlope` (gable, duo-pitch, mono-pitch — full rules in the SIP Roof Construction skill)
- Floor/ceiling decks (inter-storey cassettes) → `FloorDeck` (NOT `Deck`, `FloorCassette`, `Cassette`, or any other name)
- Foundations → `Foundation`

**Do NOT add `import Part`, `import math`, or `from FreeCAD import Vector, Placement, Rotation` to ElixiFree scripts.** The builder handles all geometry internally; the assembly layer (Placer) owns all world positioning and rotation, including the roof pitch tilt.

## Ending every script

Use `result.add_to_doc("Body")` — this handles doc setup, recompute, and fitAll in one call.
Do NOT write `doc.addObject`, `doc.recompute()`, or `FreeCAD.Gui` lines manually.

## Stock constants — use `sip_constants()` only when you need derived dimensions

Only call `sip_constants()` when your script needs to do arithmetic with SIP dimensions (e.g., computing coordinates that depend on total thickness). Do NOT call it just to name a constant you won't use in geometry.

```python
from elixifree.domains.sip import sip_constants
c = sip_constants("SIP-100")
# c["face"]=11, c["core"]=100, c["total"]=122, c["groove_width"]=45, c["groove_depth"]=50
```

For simple walls with openings, you almost never need `sip_constants()` — the builder handles all internal dimensions.

## Wall — plain

`Wall(span, height, stock, height_end=None)` — parameters are `span`, `height`, `stock`, and optional `height_end`. There is NO `length`, `width`, or `depth` parameter.

Use `height_end` for RAKED (sloped-top) walls: `height` is the wall height at X=0,
`height_end` is the wall height at X=span. The builder cuts a wedge to produce the
sloped top face.

**A raked wall is NOT a gable wall.** A gable has a triangular apex at mid-span
(`.gable(...)`, below); a raked wall has a single straight slope from one end to the
other. They are different builders and must not be mixed:

| Roof | Which walls rake | How |
|---|---|---|
| flat / low-slope (2–5°), mono-pitch | the two walls running ALONG the fall | `height` + `height_end` |
| duo-pitch / gable / hip | the two end walls | `.gable(ridge_height=…)` |

When the goal gives a wall TWO heights end-to-end (e.g. "height 2910mm at the south
end raking to 2700mm at the north end"), that is `height=2910, height_end=2700` — do
NOT call `.gable()`, and do NOT model a separate tapered top plate or firring to
create the slope. The wall itself is the taper and the roof deck rests on it; a
separate plate would double the fall and leave a stray part above the wall.

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

result = Wall(span=4000, height=2440, stock="SIP-100").build()
result.add_to_doc("Body")
```

## Reading key values from your goal

Your goal string contains the exact numeric values you need. Always extract them before writing any code:

- **Foundation**: read `strip depth`, `strip width`, `panel thickness`, `place at Vector(...)` — use them verbatim.
- **Gable end walls**: read `eave height` → use as `height=`, read `ridge height` → use as `ridge_height=`.
- **Roof slopes**: see the SIP Roof Construction skill (`sip_roofs.md`) — do not build them from this skill's rules.

If a value is missing from your goal, derive it from the bounding box dimensions provided in the prompt.

---

## Wall — gable end wall (duo-pitch/gable roof)

End walls of a gable building must have an inverted-V triangle cut from the top so they fit under the pitched roof. Use `.gable(ridge_height, apex_x=None)`.

**MANDATORY: every east/west gable end wall of a RIDGED roof calls
`.gable(ridge_height=...)`.** Ridged = duo-pitch, gable, hip, cross-gable. A flat,
low-slope or mono-pitch roof has NO ridge and NO apex — its end walls rake instead
(`height_end`, above), so `.gable()` on them fabricates an apex that does not exist.
A gable wall with no `.gable()` call builds a plain rectangular panel that will
not fit under the pitched roof — this has been observed in the field (one
gable wall regenerated with the call, its twin without it). If your component
is a gable end wall (east/west wall of a duo-pitch/gable building), `.gable()`
is never optional, even if the goal text seems ambiguous — the ridge height and
eave height values are always injected into the goal for exactly this reason.

**Your goal will say something like:**
> "Create east gable end wall SIP panel: span 4000mm, eave_height 2871.99mm, ridge height 4810.71mm …"

Extract `eave_height` → `height=`, `ridge height` → `ridge_height=`. Both values
are computed by `Elixihub.Concept.RoofGeometry` (Elixir side) from ONE formula —
never derive them yourself, always read the goal's exact numbers:

```
eave_height (rake end)  = gable_rake_end_h  = EAVE_HEIGHT + LIFT + PT * tan(θ)
ridge_height (apex)     = underside_ridge_z = EAVE_HEIGHT + LIFT + (D/2) * tan(θ)
```

Worked reference example (EAVE_HEIGHT=2700, θ=30°, PT=142, D=7000, LIFT=90):

```
eave_height  ≈ 2871.99
ridge_height ≈ 4810.71
```

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

# Values read directly from goal: span=4000, eave_height=2871.99, ridge height=4810.71
SPAN = 4000
EAVE_HEIGHT = 2871.99
RIDGE_HEIGHT = 4810.71  # from goal: "ridge height 4810.71mm"

result = (Wall(span=SPAN, height=EAVE_HEIGHT, stock="SIP-100")
    .gable(ridge_height=RIDGE_HEIGHT)
    .build())          # default local frame — the Placer's yaw sets world orientation
result.add_to_doc("Body")
```

- `span` = the exact value in the goal. For a gable end wall this is `BUILDING_DEPTH - 2*PANEL_THICKNESS`: the eave (south/north) walls run through the corners outer-to-outer and the gable BUTTS between their inner faces, so it never spans the full building depth. Use the goal's number verbatim; do not "correct" it upward.
- `height` = the gable's OWN `eave_height` field (the rake-end height where the
  rectangular body meets the triangle) — this is NOT the eave WALLS' plain
  height; the gable wall is taller because of the roof pitch rise over PT
- `ridge_height` = Z of the gable apex — MUST come from the goal string
- `apex_x` defaults to `span/2` for a symmetric gable; set explicitly for asymmetric

**Cannot combine `.gable()` with `height_end`** — use one or the other.

## Wall — sloped top (gable wall under mono-pitch roof)

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

# height at X=0 is 2440, height at X=span is 2590
result = Wall(span=3716, height=2440, stock="SIP-120", height_end=2590).build()
result.add_to_doc("Body")
```

## Wall — with door or window

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

result = (Wall(span=4000, height=2440, stock="SIP-100")
    .opening(x=1500, z=0, width=900, height=2100)
    .build())
result.add_to_doc("Body")
```

`.opening(x, z, width, height)` — x from left edge, z from bottom (z=0 for door).
Chain multiple `.opening()` calls for multiple openings.

## Wall — orientation (east/west walls)

**`.orient("Y")` is for standalone `.build()` use only — pipeline-generated wall
scripts do NOT call it.** For SIP component-generation agents, every wall script
is expanded through `Wall.construct()` (the constructability layer rewrites
`.build()` → `.construct(profile)` before execution — see
`Elixihub.Constructability.Handlers.Sip`). `_WallConstructor` always builds
panels span-along-local-X, thickness-along-local-Y, **unconditionally** — it
never branches on `Wall._orientation` — because the assembly Placer applies the
wall's world rotation (yaw) externally and identically regardless of the wall's
compass direction. Calling `.orient("Y")` pre-rotates the shape a SECOND time
inside `_build_geometry()`; combined with the Placer's own yaw this double-rotates
an east/west wall. **Do not call `.orient("Y")` in component-generation scripts.**
Build every wall in its default local frame and let the Placer's yaw put it on
the correct world axis.

`.orient(axis)` still exists in the Python API and is legitimate for a
standalone script that calls `.build()` directly (outside the pipeline, e.g. a
one-off model with no Placer) and needs the whole assembled wall body pre-rotated
in local space. It is NOT part of the pipeline convention — do not add it to
scripts destined for constructability expansion.

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

# Goal: "6000mm deep (Y direction) × 2700mm high × 142mm thick" → east/west wall
result = (Wall(span=6000, height=2440, stock="SIP-100")
    .build())          # default local frame — do NOT call .orient("Y") here
result.add_to_doc("Body")
# Local BoundBox: X=6000 (span), Y=142 (thickness), Z=2440 (height)
# The Placer rotates this into the correct world orientation for an east/west wall.
```

## Wall — inner face grooves (spline receivers)

When a perpendicular wall's spline needs to slot into this wall's face, use `.inner_groove()`:

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

result = (Wall(span=4000, height=2440, stock="SIP-100")
    .inner_groove(x=61)       # groove at left end (TOTAL_THICKNESS/2 from edge)
    .inner_groove(x=3939)     # groove at right end
    .build())
result.add_to_doc("Body")
```

`.inner_groove(x, width=45, depth=50)` — x is distance from left edge to groove centre.

## Roof panel — FLAT ROOFS ONLY (`RoofPanel`)

**`RoofPanel` is for flat roofs only.** It builds a horizontal flat box regardless of any taper.
For pitched roofs (gable, duo-pitch, mono-pitch) use `PitchedRoofSlope` — full rules in the SIP Roof Construction skill (`sip_roofs.md`).

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

result = RoofPanel(span=4200, depth=3200, stock="SIP-150").build()
result.add_to_doc("Body")
```

## Floor / ceiling deck — `FloorDeck` (inter-storey cassettes)

**Any component described as a floor deck, ceiling deck, inter-storey deck, or floor
cassette uses `FloorDeck` — there is NO `elixifree.domains.floor`, `FloorCassette`, or
`Deck` module/class. Use only `from elixifree.domains.sip import FloorDeck`.**

`FloorDeck` extrudes the storey footprint polygon up to the SIP total thickness (like
`FlatRoof`, but it is the horizontal deck separating two storeys). It bears on the lower
storey's wall tops; the Placer lifts it to the correct Z. Do NOT set `feature.Placement`.

**Parameters:**
- `vertices` — the storey footprint as an ordered list of `[x, y]` points in mm
  (closed loop; a rectangle is four corners). Read the footprint from your goal.
- `stock` — SIP stock key (e.g. `"SIP-200"`), default `"SIP-200"`.

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

# Rectangular footprint 6000 (X) x 4000 (Y), read from the goal
result = FloorDeck(
    vertices=[[0, 0], [6000, 0], [6000, 4000], [0, 4000]],
    stock="SIP-200",
).build()
result.add_to_doc("Body")
```

## Pitched roof slope — `PitchedRoofSlope` (covered by the SIP Roof Construction skill)

**NON-NEGOTIABLE: Any component described as a "pitched", "gable", "sloped", or "angled" roof panel MUST use `PitchedRoofSlope`. NEVER use `RoofPanel` for a pitched roof — it will always be flat.**

Roof slope construction rules (`ridge_length`, `half_span`, `pitch_degrees`,
`eave_overhang`, `rake_overhang` — with formulas and worked examples) live in the
**SIP Roof Construction** skill (`sip_roofs.md`), which is injected to agents
building roof components. This wall skill deliberately does not duplicate them —
an earlier copy here drifted stale (wrong `half_span` formula, missing overhang
parameters) and caused misbuilt roof panels (ISSUE-005).

## Foundation

**Slab** (solid flat plate):
```python
from elixifree.domains.sip import Foundation

result = Foundation(length=6000, width=4000, depth=150).build()
result.add_to_doc("Body")
```

**Strip / perimeter** (hollow rectangular ring — use when the description says "strip", "perimeter", "rectangular frame", or "continuous frame"):

**Your goal will say something like:**
> "Create strip perimeter foundation: 6000×4000mm footprint, strip depth 300mm, strip width 450mm, wall panel thickness 122mm. Total slab 6244×4244mm. Place at Vector(-122, -122, -300)."

Extract every value from the goal. The `Place at Vector(...)` in the goal is the mandatory `feature.Placement` offset — copy it verbatim.

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

# === PARAMETERS — read directly from goal string ===
BUILDING_WIDTH = 6000     # "6000×4000mm footprint"
BUILDING_DEPTH = 4000
PANEL_THICKNESS = 122     # "wall panel thickness 122mm"
STRIP_D = 300             # "strip depth 300mm"
STRIP_W = 450             # "strip width 450mm"

result = Foundation(
    length=BUILDING_WIDTH + 2 * PANEL_THICKNESS,
    width=BUILDING_DEPTH + 2 * PANEL_THICKNESS,
    depth=STRIP_D,
    type="strip_foundation",
    strip_w=STRIP_W,
).build()
feature = result.add_to_doc("Body")
# MANDATORY: offset from goal — "Place at Vector(-122, -122, -300)"
feature.Placement = Placement(Vector(-PANEL_THICKNESS, -PANEL_THICKNESS, -STRIP_D), Rotation())
```

`type="strip_foundation"` is required whenever the foundation is described as a perimeter ring rather than a solid slab. Default `strip_w` is 450mm if not specified.

## Stock values

| Name    | Total thickness |
|---------|----------------|
| SIP-100 | 122mm          |
| SIP-120 | 142mm          |
| SIP-150 | 172mm          |
| SIP-200 | 222mm (default)|
| SIP-250 | 272mm          |
| SIP-300 | 322mm          |

**CRITICAL — stock name rule:** Use the exact stock name from the design profile (`SIP-100`, `SIP-120`, etc.).
**NEVER derive a stock name from a thickness value.** A 142mm wall is `SIP-120`, not `SIP-142`.
The name is the core thickness, not the total panel thickness.

## Rules

1. **Always use a builder.** Do not call raw `Part.makeBox` for standard components.
2. **End every script with `result.add_to_doc("Body")`** — never write doc/recompute/Gui lines.
3. **Do not add plates, panel splits, or framing** — the assembly pipeline handles those.
4. If the builder cannot express the geometry, use raw `Part` as a fallback and add a
   comment: `# ElixiFree gap: <description>` so it can be tracked for future development.
5. **Never call `.corner_spline()`.** Splines and locking plates are separate assembly parts created during constructability — they are NOT part of the wall panel component. Wall ends always have open edge grooves (built-in automatically); never add protruding splines to the wall body.
6. **Never pass a thickness integer as `stock`.** `stock` is always a string: `"SIP-120"`, `"SIP-100"`, etc.
7. **`build()` takes no arguments.** Call `result = Wall(...).build()` with no args, then `result.add_to_doc("Body")` on the next line. Never call `.build("Body")` or `.build(doc)`.
8. **Pitched roof slopes MUST use `PitchedRoofSlope`, not `RoofPanel`.** Any component described as "pitched", "gable", "sloped", "angled", or with a degree pitch value is a pitched roof — use `PitchedRoofSlope`. A script that uses `RoofPanel` for a pitched roof will render flat.
9. **`PitchedRoofSlope` scripts MUST NOT set `feature.Placement`.** The roof panel is modelled flat in the local frame. Call `result.add_to_doc("Body")` and stop — the assembly layer (Placer) applies pitch rotation from the design spec. Never set `feature.Placement` for world positioning in a roof slope script.
10. **Gable end walls MUST use `.gable(ridge_height)` — no exceptions, ever.** Any east/west end wall of a gable (duo-pitch) building must call `.gable(ridge_height=<ridge_z>)` so the top of the wall follows the roof pitch. The `ridge_height` and `height` (eave_height) are both computed by `Elixihub.Concept.RoofGeometry` and injected verbatim into the goal — read them from there, never guess or omit the call. A plain wall will be rectangular and will not fit under the pitched roof. Cannot combine with `height_end`.
11. **Angled eave top plate (Fig 1.4.7): polygon-profile extrude, NEVER `Part.makeWedge`.** When the goal asks the eave wall to carry the Fig 1.4.7 angled eave top plate, build the wall with `Wall(...)` first, then add the plate as an `# ElixiFree gap:` raw-Part fallback. `Part.makeWedge`'s argument order is not two box corners — miscalling it fails with "delta y of wedge too small". Build the raked plate from its YZ cross-section and extrude along X:

```python
# ElixiFree gap: Wall() has no builder for the Fig 1.4.7 angled eave top plate.
import Part, math
eave_base_z = HEIGHT + TOP_FLAT_PLATE_HEIGHT          # flat top plate below
high_edge = LOW_EDGE_H + WIDTH * math.tan(math.radians(PITCH_DEG))
profile = Part.makePolygon([
    Vector(0, 0,     eave_base_z),
    Vector(0, WIDTH, eave_base_z),
    Vector(0, WIDTH, eave_base_z + LOW_EDGE_H),        # low edge at outer face
    Vector(0, 0,     eave_base_z + high_edge),         # high edge at inner face
    Vector(0, 0,     eave_base_z),
])
eave_plate = Part.Face(profile).extrude(Vector(LENGTH, 0, 0))
shape = shape.fuse(eave_plate)
```
v0.0.985